mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-19 01:13:56 +00:00
Compare commits
569 Commits
v1.0.0-dev
...
v11.0.2-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
2432fde6bf | ||
|
|
49c173dc14 | ||
|
|
d83e9372bb | ||
|
|
7e8cd3bede | ||
|
|
d67436271d | ||
|
|
aa07f35f06 | ||
|
|
77e0536838 | ||
|
|
a49e78234b | ||
|
|
a3ae825e48 | ||
|
|
146c8504ed | ||
|
|
2eb125ad69 | ||
|
|
6e24a85eab | ||
|
|
e4c3e9ffc5 | ||
|
|
4c1778a62f | ||
|
|
d99261cdbb | ||
|
|
ac1c0f2773 | ||
|
|
eddd4ec7ac | ||
|
|
07a2829c65 | ||
|
|
3d77e299d9 | ||
|
|
f1336f89e4 | ||
|
|
0502f84c20 | ||
|
|
058d292ad5 | ||
|
|
1029d56a52 | ||
|
|
709b5a0fec | ||
|
|
e1accc5041 | ||
|
|
6dbbf2e03e | ||
|
|
16557eeab0 | ||
|
|
6bca3e2bb5 | ||
|
|
a263fdfd41 | ||
|
|
e4b4bacae8 | ||
|
|
cbc97af155 | ||
|
|
d5533788e2 | ||
|
|
5a4ea5cd7d | ||
|
|
70f3c8b38c | ||
|
|
6b410a0eea | ||
|
|
73a013d75b | ||
|
|
7159f3db4c | ||
|
|
7d5ecf095c | ||
|
|
fa015a424d | ||
|
|
dd7dd38357 | ||
|
|
22356f2d26 | ||
|
|
66701f6076 | ||
|
|
6a6ded084e | ||
|
|
5887c69bde | ||
|
|
4102f43b8a | ||
|
|
5c09ef7837 | ||
|
|
3e0bf8c863 | ||
|
|
8f3ecc318c | ||
|
|
365da96e2b | ||
|
|
cd68ec4803 | ||
|
|
35265e029c | ||
|
|
9f0a09a756 | ||
|
|
e802141df5 | ||
|
|
abebc0862c | ||
|
|
96ef150e89 | ||
|
|
c5de9e2988 | ||
|
|
c391ca648b | ||
|
|
7cf79e68e0 | ||
|
|
f07db3c214 | ||
|
|
88bb3a8845 | ||
|
|
b9e6bd6775 | ||
|
|
cd1b72e078 | ||
|
|
6b889557ab | ||
|
|
4b1be8c647 | ||
|
|
73c893c6e7 | ||
|
|
75b36823b8 | ||
|
|
d2d93cd075 | ||
|
|
26b8621ac8 | ||
|
|
f365a41741 | ||
|
|
9ec720e983 | ||
|
|
0f432b3fdd | ||
|
|
96cd5618dd | ||
|
|
c2a5a55e67 | ||
|
|
6c5de8b414 | ||
|
|
ea773cfa56 | ||
|
|
a306561b55 | ||
|
|
b6dcd88495 | ||
|
|
a925650044 | ||
|
|
77bbf6be1f | ||
|
|
bd053b7e99 | ||
|
|
fd742eba63 | ||
|
|
ba9d998681 | ||
|
|
75df245ec3 | ||
|
|
4164cb0dea | ||
|
|
18fe35ae73 | ||
|
|
f9bc95f220 | ||
|
|
d2f91a8545 | ||
|
|
4016bdc37f | ||
|
|
538b2a8599 | ||
|
|
4aa14bbb85 | ||
|
|
d37452997b | ||
|
|
db21d5e953 | ||
|
|
4d581811db | ||
|
|
8c502448be | ||
|
|
fec16d9442 | ||
|
|
5583904994 | ||
|
|
797286b758 | ||
|
|
4ae9ad09d6 | ||
|
|
447e1ad30e | ||
|
|
843e62ad29 | ||
|
|
9c07ffcc7a | ||
|
|
438321330e | ||
|
|
3ba4be240b | ||
|
|
98ce0abfa9 | ||
|
|
db4348c4fa | ||
|
|
4839f87519 | ||
|
|
809862c997 | ||
|
|
fd5c878cee | ||
|
|
124332f0e9 | ||
|
|
d4cf0cea52 | ||
|
|
76676fb567 | ||
|
|
d802ef844e | ||
|
|
90fc547673 | ||
|
|
3813e28ac2 | ||
|
|
a2bb4004c7 | ||
|
|
a0cb449c60 | ||
|
|
e0271790b8 | ||
|
|
4bfd7ebff8 | ||
|
|
2f7e62ef65 | ||
|
|
4485af8036 | ||
|
|
085a3a479d | ||
|
|
f75c9a78b8 | ||
|
|
172655bde0 | ||
|
|
456db7289a | ||
|
|
e722e3f4f9 | ||
|
|
c348c1f0a0 | ||
|
|
ed1851013e | ||
|
|
e31ac1f132 | ||
|
|
8f78f85e4a | ||
|
|
0be2677519 | ||
|
|
b873228ef0 | ||
|
|
639ff1c0ba | ||
|
|
f30671ddd1 | ||
|
|
76c45dd7c1 | ||
|
|
1bafb77355 | ||
|
|
25f74dc5e9 | ||
|
|
6e73631d4d | ||
|
|
7761d5b85e | ||
|
|
62aa295e73 | ||
|
|
596ede1b12 | ||
|
|
7debe62738 | ||
|
|
002f84da1a | ||
|
|
aff4968e6f | ||
|
|
1d989abd55 | ||
|
|
f1775f83d0 | ||
|
|
4055939c08 | ||
|
|
85120374d6 | ||
|
|
8b4819faa1 | ||
|
|
d219276298 | ||
|
|
79f91e0e5a | ||
|
|
fadf62f594 | ||
|
|
ad3d332e27 | ||
|
|
8f66df7666 | ||
|
|
80c2e80925 | ||
|
|
c3db23d3c7 | ||
|
|
c28584736e | ||
|
|
6b909c1ee6 | ||
|
|
0e8446516e | ||
|
|
aa46b953db | ||
|
|
a562e476c0 | ||
|
|
75d2be8803 | ||
|
|
d6308e126c | ||
|
|
bb97af4d86 | ||
|
|
392164862c | ||
|
|
53e807dec1 | ||
|
|
288d50a8b4 | ||
|
|
131dedd4b0 | ||
|
|
5a92d5c29d | ||
|
|
4b81318710 | ||
|
|
44f6a3ebc5 | ||
|
|
7882a8d928 | ||
|
|
cc3d32748b | ||
|
|
f9da2ad531 | ||
|
|
b19e1131e8 | ||
|
|
123ad54c15 | ||
|
|
09f6ab4155 | ||
|
|
01cf3fb50f | ||
|
|
6c5f9d4198 | ||
|
|
7b2d058144 | ||
|
|
db2804270e | ||
|
|
2572cd04b5 | ||
|
|
5eb8b428b9 | ||
|
|
3a118d9b9d | ||
|
|
14a73bfcaf | ||
|
|
567bf52e16 | ||
|
|
35c6489dba | ||
|
|
371f0c4d0b | ||
|
|
1b42f65d95 | ||
|
|
2aee0cbd0f | ||
|
|
19256b5437 | ||
|
|
67a5237541 | ||
|
|
4e2e772389 | ||
|
|
799bc9e163 | ||
|
|
2431785d0e | ||
|
|
fb3c0e87d4 | ||
|
|
5f7ef2dbff | ||
|
|
ec1d8a8fba | ||
|
|
814ce0b9ae | ||
|
|
8d95b14f35 | ||
|
|
711b8a25a7 | ||
|
|
fbb09f38dc | ||
|
|
9a4d30e152 | ||
|
|
368c61c1bf | ||
|
|
bbd40bf2f6 | ||
|
|
3de999a2d3 | ||
|
|
83cbb2f110 | ||
|
|
fcc7fa75d0 | ||
|
|
495ebface8 | ||
|
|
8c12f8d488 | ||
|
|
523f67b238 | ||
|
|
4813a8b48e | ||
|
|
d5c66022c9 | ||
|
|
1d4034b36c | ||
|
|
dd2d696d00 | ||
|
|
6326321b65 | ||
|
|
f291a4ae3e | ||
|
|
817b8db019 | ||
|
|
a321b8971b | ||
|
|
783b2de9db | ||
|
|
77604d4078 | ||
|
|
21b5404180 | ||
|
|
9ac6d5c7da | ||
|
|
1b39278b24 | ||
|
|
0ebab8bf59 | ||
|
|
112bc998f4 | ||
|
|
12c96bf818 | ||
|
|
91298a8790 | ||
|
|
f2a7cff41c | ||
|
|
dd941233ca | ||
|
|
fc06dd1c29 | ||
|
|
482af78f2b | ||
|
|
89a27dfbe6 | ||
|
|
4ea030d0a0 | ||
|
|
4cc2fa17f5 | ||
|
|
48068cb3d7 | ||
|
|
d107c7245c | ||
|
|
4b2e3230ec | ||
|
|
fb5b82da4e | ||
|
|
5970e32aa5 | ||
|
|
0f00d33f4e | ||
|
|
83187c9edd | ||
|
|
79d70cff4b | ||
|
|
6f72c4c4c0 | ||
|
|
c8eedac4d9 | ||
|
|
60a8278ae8 | ||
|
|
ada5a033de | ||
|
|
109b8a296d | ||
|
|
e2faf4ca9b | ||
|
|
2134182a0e | ||
|
|
d8b5b8bb7c | ||
|
|
49970b5926 | ||
|
|
c1fbd8cf8c | ||
|
|
a467fbb704 | ||
|
|
5a4bd7a76e | ||
|
|
e5ca86fac6 | ||
|
|
68d9e9f02c | ||
|
|
494a9a09ac | ||
|
|
06a88839de | ||
|
|
4c41b955df | ||
|
|
614e555f4c | ||
|
|
6bfe5716c3 | ||
|
|
d6ed06a327 | ||
|
|
2fc4ec4021 | ||
|
|
f565c4f6a7 | ||
|
|
35749454ab | ||
|
|
2b492e7a5e | ||
|
|
081a5a6849 | ||
|
|
852ae7d8d1 | ||
|
|
52f8a6a2eb | ||
|
|
efa8ea1445 | ||
|
|
c828fa2a27 | ||
|
|
92ac5e4dc2 | ||
|
|
070c09cf71 | ||
|
|
b738dcd7ea | ||
|
|
8efcf329bb | ||
|
|
0e87ef56c4 | ||
|
|
e3bf367ad6 | ||
|
|
3d61dacbda | ||
|
|
c20dfe12d5 | ||
|
|
78663cde88 | ||
|
|
dde5385232 | ||
|
|
930768dfb3 | ||
|
|
1f4bc5079f | ||
|
|
caf2745805 | ||
|
|
a4529c3fee | ||
|
|
835c0f9f7a | ||
|
|
34f607aa24 | ||
|
|
0f38b94701 | ||
|
|
39bb1b25dc | ||
|
|
4fc63a4d8a | ||
|
|
6037397bc2 | ||
|
|
273dd86b65 | ||
|
|
e8488b3e86 | ||
|
|
c13361823d | ||
|
|
8f66f9f606 | ||
|
|
a123026f46 | ||
|
|
9c39c9efdb | ||
|
|
3ee1c01430 | ||
|
|
64bae884dc | ||
|
|
e94a706949 | ||
|
|
89bb43066b | ||
|
|
68174bbd6b | ||
|
|
d05c9416d6 | ||
|
|
5a2f02b97d | ||
|
|
a3005fa08e | ||
|
|
6628b7870f | ||
|
|
a6411245aa | ||
|
|
365e1d7a45 | ||
|
|
4507cd2353 | ||
|
|
1f75777cf9 | ||
|
|
28d5468b07 | ||
|
|
746496125d | ||
|
|
835a421cc0 | ||
|
|
99342fe033 | ||
|
|
e47b67d7ec | ||
|
|
ad6c5c8273 | ||
|
|
fd690acd61 | ||
|
|
e698b02bf6 | ||
|
|
ab866bb8ef | ||
|
|
714a98422d | ||
|
|
051afd98d0 | ||
|
|
d38cf6a229 | ||
|
|
03f5ee088b | ||
|
|
5d0fd48b15 | ||
|
|
56f6ca3891 | ||
|
|
9e0a74fcfb | ||
|
|
8f3ac7702a | ||
|
|
7b65f2d02c | ||
|
|
2a1b2df56b | ||
|
|
dfd8a24512 | ||
|
|
a3efd212fc | ||
|
|
3e610f7ba9 | ||
|
|
cff87ff077 | ||
|
|
54aa04ca34 | ||
|
|
0eda84eaef | ||
|
|
b68b0bf3d7 | ||
|
|
c692202f67 | ||
|
|
8ce3535427 | ||
|
|
6a5c8735fb | ||
|
|
8f32bc9c08 | ||
|
|
d3a580ea19 | ||
|
|
4c8eb0e5c7 | ||
|
|
bc92eb7fd8 | ||
|
|
cf89bd4171 | ||
|
|
566ecefa2b | ||
|
|
8eb4a8f87a | ||
|
|
afcba5c212 | ||
|
|
2dcbd8d079 | ||
|
|
81895c7d5c | ||
|
|
22267883b1 | ||
|
|
26fca60b53 | ||
|
|
31815ca9ea | ||
|
|
1ce6098cad | ||
|
|
46a6396114 | ||
|
|
41e88605c3 | ||
|
|
893d4c699b | ||
|
|
bf8655ead8 | ||
|
|
6c65952d80 | ||
|
|
26f3e7336b | ||
|
|
849616dc2b | ||
|
|
e8a131fb08 | ||
|
|
11abc67d9a | ||
|
|
8615798711 | ||
|
|
070e02ea28 | ||
|
|
54511a4fc6 | ||
|
|
94f3c9116e | ||
|
|
fe56c0cadb | ||
|
|
bf4894592b | ||
|
|
5a96f2d99f | ||
|
|
839a5ef22a | ||
|
|
0ebdb100fd | ||
|
|
4f60bea81e | ||
|
|
1a49d9439f | ||
|
|
5535eb4f01 | ||
|
|
5dc6aa9bfd | ||
|
|
6c0d28191b | ||
|
|
9d067b9b0d | ||
|
|
a4d8be20fc | ||
|
|
ec9fd15f9b | ||
|
|
d20f7fd6e1 | ||
|
|
ebbcf78e56 | ||
|
|
024fa867e1 | ||
|
|
e506d8fd8c | ||
|
|
f5b5c52e55 | ||
|
|
fc05fe79de | ||
|
|
79909cf260 | ||
|
|
8f12873200 | ||
|
|
cc9416dd11 | ||
|
|
99319e63da | ||
|
|
c459beb5f8 | ||
|
|
e5ae970009 | ||
|
|
612515acf8 | ||
|
|
b1eebc99a7 | ||
|
|
6cb7cdb0b2 | ||
|
|
beff1df9b0 | ||
|
|
f58a498849 | ||
|
|
6cb1fdf617 | ||
|
|
7399450139 | ||
|
|
193eae298a | ||
|
|
4e56652429 | ||
|
|
7755bbc645 | ||
|
|
f3b5f67b39 | ||
|
|
7f18bbf66e | ||
|
|
c8b68e36e0 | ||
|
|
5ca5a1c29e | ||
|
|
d677d9e800 | ||
|
|
e5bea06353 | ||
|
|
8f1a629191 | ||
|
|
e6086511e5 | ||
|
|
c612676543 | ||
|
|
bca948658e | ||
|
|
0e5f4ba2d5 | ||
|
|
6ca05769ef | ||
|
|
c21e5affba | ||
|
|
1c5a04caf9 | ||
|
|
3d6a1d38f3 | ||
|
|
715a2ad025 | ||
|
|
9889ec9d03 | ||
|
|
01bfbd656e | ||
|
|
6c9797583d | ||
|
|
3cf07f5ce2 | ||
|
|
0bfb92a0cb | ||
|
|
042638a399 | ||
|
|
4178a1eedc | ||
|
|
338bd9f739 | ||
|
|
c55c62a57e | ||
|
|
1f08da8b2a | ||
|
|
b2dab3fabf | ||
|
|
0f30eac32c | ||
|
|
642e9031eb | ||
|
|
7a56dca004 | ||
|
|
18853f70a4 | ||
|
|
6b8b0573d4 | ||
|
|
3889d72927 | ||
|
|
3f97cc8e1f | ||
|
|
1db735b1e2 | ||
|
|
996c4acb20 | ||
|
|
5b28523eea | ||
|
|
72f3cad3f9 | ||
|
|
9659a61c5c | ||
|
|
b892729332 | ||
|
|
6e4db110c8 | ||
|
|
e810197e2a | ||
|
|
e65ebd27c2 | ||
|
|
5bd416b409 | ||
|
|
a6c6b4979a | ||
|
|
db10ab03be | ||
|
|
94dbb573cf | ||
|
|
08253ee010 | ||
|
|
3144ec872a | ||
|
|
48c4ea2f6d | ||
|
|
c63b20fa65 | ||
|
|
f806cb38c5 | ||
|
|
6c3e2d79ea | ||
|
|
cca12aa34a | ||
|
|
c267b12a7d | ||
|
|
eef448cc39 | ||
|
|
45303f66ec | ||
|
|
783fbf43f2 | ||
|
|
59189058ac | ||
|
|
dfac8f03a3 | ||
|
|
b44bf4c267 | ||
|
|
066ad274ed | ||
|
|
a8653fe6a0 | ||
|
|
139a23b750 | ||
|
|
0d8d19e708 | ||
|
|
a1e909b163 | ||
|
|
e4157332d3 | ||
|
|
923efc4caf | ||
|
|
bea0cbc550 | ||
|
|
1ee2e4ba56 | ||
|
|
fac44a50c3 | ||
|
|
544bcf76bd | ||
|
|
a16c8cabf2 | ||
|
|
2777117da2 | ||
|
|
66a9b76845 | ||
|
|
f068fc87ff | ||
|
|
7cc8a7dec3 | ||
|
|
398239dc10 | ||
|
|
d18a3b6a28 | ||
|
|
bfe4e3e298 | ||
|
|
a1b6b06bd3 | ||
|
|
4087f49863 | ||
|
|
00c85b5d75 | ||
|
|
cb78c5a86f | ||
|
|
9991f39c9a | ||
|
|
650bf71124 | ||
|
|
1dd3394ea3 | ||
|
|
e6c2501539 | ||
|
|
4b26305bd5 | ||
|
|
ce21bd60f3 | ||
|
|
3651981161 | ||
|
|
6299b9e951 | ||
|
|
24b544708f | ||
|
|
c7ef2644d8 | ||
|
|
fa6e454ae9 | ||
|
|
cbd8df2df0 | ||
|
|
f3d8b917de | ||
|
|
82c530650f | ||
|
|
81d0cf20f3 | ||
|
|
36d4f71325 | ||
|
|
699c730a7c | ||
|
|
b197956e39 | ||
|
|
be992a3e6f | ||
|
|
e0437397df | ||
|
|
41749ba829 | ||
|
|
8161ce4fa6 | ||
|
|
6626014ef3 | ||
|
|
e6e468fbb5 | ||
|
|
2fabbdf71b | ||
|
|
7faa001406 | ||
|
|
70872307e3 | ||
|
|
310a7c446b | ||
|
|
c236ebe078 | ||
|
|
674461f08d | ||
|
|
bbb2c547aa | ||
|
|
b957501e70 | ||
|
|
b6ca31a970 | ||
|
|
cbcf93f7d6 | ||
|
|
d5b4c99c00 | ||
|
|
4dd820ffdf | ||
|
|
cb9b1b9416 | ||
|
|
428f7f4dec | ||
|
|
77536cce8f | ||
|
|
7b6a61b674 | ||
|
|
7f5f3b217d |
72
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
72
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 📃 Documentation
|
||||||
|
url: https://github.com/revanced/revanced-documentation/
|
||||||
|
about: Don't know how or where to start? Check out our documentation!
|
||||||
|
- name: 🗨 Discussions
|
||||||
|
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||||
|
about: Got something you think should change or be added? Search for or start a new discussion!
|
||||||
58
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
2
.github/config.yml
vendored
Normal file
2
.github/config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
firstPRMergeComment: >
|
||||||
|
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
|
||||||
25
.github/workflows/pull_request.yml
vendored
Normal file
25
.github/workflows/pull_request.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: PR to main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pull-request:
|
||||||
|
name: Open pull request
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Open pull request
|
||||||
|
uses: repo-sync/pull-request@v2
|
||||||
|
with:
|
||||||
|
destination_branch: 'main'
|
||||||
|
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||||
|
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||||
|
pr_draft: true
|
||||||
45
.github/workflows/release.yml
vendored
Normal file
45
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# Make sure the release step uses its own credentials:
|
||||||
|
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ runner.home }}/.gradle/caches
|
||||||
|
${{ runner.home }}/.gradle/wrapper
|
||||||
|
.gradle
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
|
||||||
|
- name: Build with Gradle
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew clean --no-daemon
|
||||||
|
- name: Setup semantic-release
|
||||||
|
run: npm install
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||||
|
run: npm exec semantic-release
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -74,6 +74,7 @@ cmake-build-*/
|
|||||||
|
|
||||||
# IntelliJ
|
# IntelliJ
|
||||||
out/
|
out/
|
||||||
|
.idea/
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
# mpeltonen/sbt-idea plugin
|
||||||
.idea_modules/
|
.idea_modules/
|
||||||
@@ -115,3 +116,9 @@ gradle-app.setting
|
|||||||
|
|
||||||
# Avoid ignoring test resources
|
# Avoid ignoring test resources
|
||||||
!src/test/resources/*
|
!src/test/resources/*
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Gradle props, to avoid sharing the gpr key
|
||||||
|
gradle.properties
|
||||||
|
|||||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
10
.idea/codeStyles/Project.xml
generated
10
.idea/codeStyles/Project.xml
generated
@@ -1,10 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<code_scheme name="Project" version="173">
|
|
||||||
<JetCodeStyleSettings>
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</JetCodeStyleSettings>
|
|
||||||
<codeStyleSettings language="kotlin">
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</codeStyleSettings>
|
|
||||||
</code_scheme>
|
|
||||||
</component>
|
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<state>
|
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
||||||
7
.idea/discord.xml
generated
7
.idea/discord.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DiscordProjectSettings">
|
|
||||||
<option name="show" value="PROJECT_FILES" />
|
|
||||||
<option name="description" value="" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
10
.idea/misc.xml
generated
10
.idea/misc.xml
generated
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="azul-17" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
37
.releaserc
Normal file
37
.releaserc
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main",
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"prerelease": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
"gradle-semantic-release-plugin",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"gradle.properties"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
backmergeBranches: [{"from": "main", "to": "dev"}],
|
||||||
|
clearWorkspace: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
successComment: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
1394
CHANGELOG.md
Normal file
1394
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +1,3 @@
|
|||||||
# Patcher
|
# 💉 ReVanced Patcher
|
||||||
|
|
||||||
|
ReVanced Patcher used to patch Android applications.
|
||||||
|
|||||||
@@ -1,42 +1,67 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.6.10"
|
kotlin("jvm") version "1.8.10"
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.revanced"
|
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 {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
||||||
|
credentials {
|
||||||
|
username = githubUsername
|
||||||
|
password = githubPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib"))
|
implementation("xpp3:xpp3:1.1.4c")
|
||||||
implementation("org.ow2.asm:asm:9.2")
|
implementation("app.revanced:smali:2.5.3-a3836654")
|
||||||
implementation("org.ow2.asm:asm-util:9.2")
|
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
|
||||||
implementation("org.ow2.asm:asm-tree:9.2")
|
implementation("app.revanced:apktool-lib:2.7.0")
|
||||||
implementation("org.ow2.asm:asm-commons:9.2")
|
|
||||||
implementation("io.github.microutils:kotlin-logging:2.1.21")
|
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
|
||||||
testImplementation("ch.qos.logback:logback-classic:1.2.11") // use your own logger!
|
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
|
||||||
testImplementation(kotlin("test"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks {
|
||||||
useJUnitPlatform()
|
test {
|
||||||
testLogging {
|
useJUnitPlatform()
|
||||||
events("PASSED", "SKIPPED", "FAILED")
|
testLogging {
|
||||||
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
processResources {
|
||||||
|
expand("projectVersion" to project.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
if (System.getenv("GITHUB_ACTOR") != null)
|
||||||
name = "GitHubPackages"
|
maven {
|
||||||
url = uri("https://maven.pkg.github.com/ReVancedTeam/revanced-patcher")
|
name = "GitHubPackages"
|
||||||
credentials {
|
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||||
username = System.getenv("GITHUB_ACTOR")
|
credentials {
|
||||||
password = System.getenv("GITHUB_TOKEN")
|
username = System.getenv("GITHUB_ACTOR")
|
||||||
|
password = System.getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
mavenLocal()
|
||||||
}
|
}
|
||||||
publications {
|
publications {
|
||||||
register<MavenPublication>("gpr") {
|
register<MavenPublication>("gpr") {
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style = official
|
||||||
version=1.0.0
|
version = 11.0.2-dev.1
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
269
gradlew
vendored
Normal file → Executable file
269
gradlew
vendored
Normal file → Executable file
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright 2015 the original author or authors.
|
# Copyright <20> 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,67 +17,101 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
#
|
||||||
## Gradle start up script for UN*X
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
##
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions <20>$var<61>, <20>${var}<7D>, <20>${var:-default}<7D>, <20>${var+SET}<7D>,
|
||||||
|
# <20>${var#prefix}<7D>, <20>${var%suffix}<7D>, and <20>$( cmd )<29>;
|
||||||
|
# * compound commands having a testable exit status, especially <20>case<73>;
|
||||||
|
# * various built-in commands including <20>command<6E>, <20>set<65>, and <20>ulimit<69>.
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# 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
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
PRG="$0"
|
app_path=$0
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
# Need this for daisy-chained symlinks.
|
||||||
ls=`ls -ld "$PRG"`
|
while
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
[ -h "$app_path" ]
|
||||||
PRG="$link"
|
do
|
||||||
else
|
ls=$( ls -ld "$app_path" )
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
link=${ls#*' -> '}
|
||||||
fi
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=`basename "$0"`
|
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.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD=maximum
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "`uname`" in
|
case "$( uname )" in #(
|
||||||
CYGWIN* )
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
cygwin=true
|
Darwin* ) darwin=true ;; #(
|
||||||
;;
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
Darwin* )
|
NONSTOP* ) nonstop=true ;;
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MSYS* | MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD="java"
|
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.
|
which java >/dev/null 2>&1 || 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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
@@ -106,80 +140,95 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
case $MAX_FD in #(
|
||||||
if [ $? -eq 0 ] ; then
|
max*)
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
warn "Could not query maximum file descriptor limit"
|
||||||
fi
|
esac
|
||||||
ulimit -n $MAX_FD
|
case $MAX_FD in #(
|
||||||
if [ $? -ne 0 ] ; then
|
'' | soft) :;; #(
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
*)
|
||||||
fi
|
ulimit -n "$MAX_FD" ||
|
||||||
else
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=`expr $i + 1`
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
0) set -- ;;
|
|
||||||
1) set -- "$args0" ;;
|
|
||||||
2) set -- "$args0" "$args1" ;;
|
|
||||||
3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
save () {
|
# * args from the command line
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
# * the main class name
|
||||||
echo " "
|
# * -classpath
|
||||||
}
|
# * -D...appname settings
|
||||||
APP_ARGS=`save "$@"`
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
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.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|||||||
6580
package-lock.json
generated
Normal file
6580
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
package.json
Normal file
9
package.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@saithodev/semantic-release-backmerge": "^3.1.0",
|
||||||
|
"@semantic-release/changelog": "^6.0.2",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"gradle-semantic-release-plugin": "^1.7.6",
|
||||||
|
"semantic-release": "^20.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
415
src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
415
src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.Context
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.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.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
internal val NAMER = BasicDexFileNamer()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ReVanced Patcher.
|
||||||
|
* @param options The options for the patcher.
|
||||||
|
*/
|
||||||
|
class Patcher(private val options: PatcherOptions) {
|
||||||
|
private val logger = options.logger
|
||||||
|
private val opcodes: Opcodes
|
||||||
|
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
|
||||||
|
private var mergeIntegrations = false
|
||||||
|
val context: PatcherContext
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
val version = VersionReader.read()
|
||||||
|
private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
|
||||||
|
this.aaptPath = options.aaptPath
|
||||||
|
this.useAapt2 = true
|
||||||
|
this.frameworkFolderLocation = options.frameworkFolderLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
logger.info("Reading dex files")
|
||||||
|
// read dex files
|
||||||
|
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
|
||||||
|
// get the opcodes
|
||||||
|
opcodes = dexFile.opcodes
|
||||||
|
// finally create patcher context
|
||||||
|
context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory))
|
||||||
|
|
||||||
|
// decode manifest file
|
||||||
|
decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add integrations to be merged by the patcher.
|
||||||
|
* The integrations will only be merged, if necessary.
|
||||||
|
*
|
||||||
|
* @param integrations The integrations, must be dex files or dex file container such as ZIP, APK or DEX files.
|
||||||
|
* @param callback The callback for [integrations] which are being added.
|
||||||
|
*/
|
||||||
|
fun addIntegrations(
|
||||||
|
integrations: List<File>,
|
||||||
|
callback: (File) -> Unit
|
||||||
|
) {
|
||||||
|
context.integrations.apply integrations@{
|
||||||
|
add(integrations)
|
||||||
|
this@integrations.callback = callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the patched dex file.
|
||||||
|
*/
|
||||||
|
fun save(): PatcherResult {
|
||||||
|
val packageMetadata = context.packageMetadata
|
||||||
|
val metaInfo = packageMetadata.metaInfo
|
||||||
|
var resourceFile: File? = null
|
||||||
|
|
||||||
|
when (resourceDecodingMode) {
|
||||||
|
ResourceDecodingMode.FULL -> {
|
||||||
|
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
|
||||||
|
try {
|
||||||
|
val androlibResources = AndrolibResources().also { resources ->
|
||||||
|
resources.buildOptions = BuildOptions().also { buildOptions ->
|
||||||
|
buildOptions.setBuildOptions(options)
|
||||||
|
buildOptions.isFramework = metaInfo.isFrameworkApk
|
||||||
|
buildOptions.resourcesAreCompressed = metaInfo.compressionType
|
||||||
|
buildOptions.doNotCompress = metaInfo.doNotCompress
|
||||||
|
}
|
||||||
|
|
||||||
|
resources.setSdkInfo(metaInfo.sdkInfo)
|
||||||
|
resources.setVersionInfo(metaInfo.versionInfo)
|
||||||
|
resources.setSharedLibrary(metaInfo.sharedLibrary)
|
||||||
|
resources.setSparseResources(metaInfo.sparseResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
|
||||||
|
|
||||||
|
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
|
||||||
|
|
||||||
|
val aaptFile = cacheDirectory.resolve("aapt_temp_file")
|
||||||
|
|
||||||
|
// delete if it exists
|
||||||
|
Files.deleteIfExists(aaptFile.toPath())
|
||||||
|
|
||||||
|
val resDirectory = cacheDirectory.resolve("res")
|
||||||
|
val includedFiles = metaInfo.usesFramework.ids.map { id ->
|
||||||
|
androlibResources.getFrameworkApk(
|
||||||
|
id, metaInfo.usesFramework.tag
|
||||||
|
)
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
logger.info("Compiling resources")
|
||||||
|
androlibResources.aaptPackage(
|
||||||
|
aaptFile, manifestFile, resDirectory, null, null, includedFiles
|
||||||
|
)
|
||||||
|
|
||||||
|
resourceFile = aaptFile
|
||||||
|
} finally {
|
||||||
|
cacheDirectory.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> logger.info("Not compiling resources because resource patching is not required")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("Creating new dex file")
|
||||||
|
val newDexFile = object : DexFile {
|
||||||
|
override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
|
||||||
|
override fun getOpcodes() = this@Patcher.opcodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// write modified dex files
|
||||||
|
logger.info("Writing modified dex files")
|
||||||
|
val dexFiles = mutableMapOf<String, MemoryDataStore>()
|
||||||
|
MultiDexIO.writeDexFile(
|
||||||
|
true, -1, // core count
|
||||||
|
dexFiles, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
|
||||||
|
)
|
||||||
|
|
||||||
|
return PatcherResult(
|
||||||
|
dexFiles.map {
|
||||||
|
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
|
||||||
|
},
|
||||||
|
metaInfo.doNotCompress?.toList(),
|
||||||
|
resourceFile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add [Patch]es to the patcher.
|
||||||
|
* @param patches [Patch]es The patches to add.
|
||||||
|
*/
|
||||||
|
fun addPatches(patches: Iterable<Class<out Patch<Context>>>) {
|
||||||
|
/**
|
||||||
|
* Returns true if at least one patches or its dependencies matches the given predicate.
|
||||||
|
*/
|
||||||
|
fun Class<out Patch<Context>>.anyRecursively(predicate: (Class<out Patch<Context>>) -> Boolean): Boolean =
|
||||||
|
predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true
|
||||||
|
|
||||||
|
|
||||||
|
// Determine if resource patching is required.
|
||||||
|
for (patch in patches) {
|
||||||
|
if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) {
|
||||||
|
resourceDecodingMode = ResourceDecodingMode.FULL
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if merging integrations is required.
|
||||||
|
for (patch in patches) {
|
||||||
|
if (patch.anyRecursively { it.requiresIntegrations }) {
|
||||||
|
mergeIntegrations = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.patches.addAll(patches)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode resources for the patcher.
|
||||||
|
*
|
||||||
|
* @param mode The [ResourceDecodingMode] to use when decoding.
|
||||||
|
*/
|
||||||
|
private fun decodeResources(mode: ResourceDecodingMode) {
|
||||||
|
val extInputFile = ExtFile(options.inputFile)
|
||||||
|
try {
|
||||||
|
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
|
||||||
|
val resourceTable = androlib.getResTable(extInputFile, true)
|
||||||
|
when (mode) {
|
||||||
|
ResourceDecodingMode.FULL -> {
|
||||||
|
val outDir = File(options.resourceCacheDirectory)
|
||||||
|
if (outDir.exists()) {
|
||||||
|
logger.info("Deleting existing resource cache directory")
|
||||||
|
if (!outDir.deleteRecursively()) {
|
||||||
|
logger.error("Failed to delete existing resource cache directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outDir.mkdirs()
|
||||||
|
|
||||||
|
logger.info("Decoding resources")
|
||||||
|
|
||||||
|
// decode resources to cache directory
|
||||||
|
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
|
||||||
|
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
|
||||||
|
|
||||||
|
// read additional metadata from the resource table
|
||||||
|
context.packageMetadata.let { metadata ->
|
||||||
|
metadata.metaInfo.usesFramework = UsesFramework().also { framework ->
|
||||||
|
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
// read files to not compress
|
||||||
|
metadata.metaInfo.doNotCompress = buildList {
|
||||||
|
androlib.recordUncompressedFiles(extInputFile, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceDecodingMode.MANIFEST_ONLY -> {
|
||||||
|
logger.info("Decoding AndroidManifest.xml only, because resources are not needed")
|
||||||
|
|
||||||
|
// create decoder for the resource table
|
||||||
|
val decoder = ResAttrDecoder()
|
||||||
|
decoder.currentPackage = ResPackage(resourceTable, 0, null)
|
||||||
|
|
||||||
|
// create xml parser with the decoder
|
||||||
|
val axmlParser = AXmlResourceParser()
|
||||||
|
axmlParser.attrDecoder = decoder
|
||||||
|
|
||||||
|
// parse package information with the decoder and parser which will set required values in the resource table
|
||||||
|
// instead of decodeManifest another more low level solution can be created to make it faster/better
|
||||||
|
XmlPullStreamDecoder(
|
||||||
|
axmlParser, AndrolibResources().resXmlSerializer
|
||||||
|
).decodeManifest(
|
||||||
|
extInputFile.directory.getFileInput("AndroidManifest.xml"),
|
||||||
|
object : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read of the resourceTable which is created by reading the manifest file
|
||||||
|
context.packageMetadata.let { metadata ->
|
||||||
|
metadata.packageName = resourceTable.currentResPackage.name
|
||||||
|
metadata.packageVersion = resourceTable.versionInfo.versionName
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
context.patches.forEach { patch ->
|
||||||
|
val patchResult = executePatch(patch, executedPatches)
|
||||||
|
|
||||||
|
val result = if (patchResult.isSuccess()) {
|
||||||
|
Result.success(patchResult.success()!!)
|
||||||
|
} else {
|
||||||
|
Result.failure(patchResult.error()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This prints before the patch really finishes in case it is a Closeable
|
||||||
|
// because the Closeable is closed after all patches are executed.
|
||||||
|
yield(patch.patchName to result)
|
||||||
|
|
||||||
|
if (stopOnError && patchResult.isError()) return@sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
executedPatches.values
|
||||||
|
.filter(ExecutedPatch::success)
|
||||||
|
.map(ExecutedPatch::patchInstance)
|
||||||
|
.filterIsInstance(Closeable::class.java)
|
||||||
|
.asReversed().forEach {
|
||||||
|
try {
|
||||||
|
it.close()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
val patchName = (it as Patch<Context>).javaClass.patchName
|
||||||
|
|
||||||
|
logger.error("Failed to close '$patchName': ${exception.stackTraceToString()}")
|
||||||
|
|
||||||
|
yield(patchName to Result.failure(exception))
|
||||||
|
|
||||||
|
// This is not failsafe. If a patch throws an exception while closing,
|
||||||
|
// the other patches that depend on it may fail.
|
||||||
|
if (stopOnError) return@sequence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of decoding the resources.
|
||||||
|
*/
|
||||||
|
private enum class ResourceDecodingMode {
|
||||||
|
/**
|
||||||
|
* Decode all resources.
|
||||||
|
*/
|
||||||
|
FULL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the manifest file only.
|
||||||
|
*/
|
||||||
|
MANIFEST_ONLY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A result of executing a [Patch].
|
||||||
|
*
|
||||||
|
* @param patchInstance The instance of the [Patch] that was applied.
|
||||||
|
* @param success The result of the [Patch].
|
||||||
|
*/
|
||||||
|
internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)
|
||||||
64
src/main/kotlin/app/revanced/patcher/PatcherContext.kt
Normal file
64
src/main/kotlin/app/revanced/patcher/PatcherContext.kt
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.*
|
||||||
|
import app.revanced.patcher.logging.Logger
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.util.ClassMerger.merge
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
data class PatcherContext(
|
||||||
|
val classes: MutableList<ClassDef>,
|
||||||
|
val resourceCacheDirectory: File,
|
||||||
|
) {
|
||||||
|
val packageMetadata = PackageMetadata()
|
||||||
|
internal val patches = mutableListOf<Class<out Patch<Context>>>()
|
||||||
|
internal val integrations = Integrations(this)
|
||||||
|
internal val bytecodeContext = BytecodeContext(classes)
|
||||||
|
internal val resourceContext = ResourceContext(resourceCacheDirectory)
|
||||||
|
|
||||||
|
internal class Integrations(val context: PatcherContext) {
|
||||||
|
var callback: ((File) -> Unit)? = null
|
||||||
|
private val integrations: MutableList<File> = mutableListOf()
|
||||||
|
|
||||||
|
fun add(integrations: List<File>) = this@Integrations.integrations.addAll(integrations)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge integrations.
|
||||||
|
* @param logger A logger.
|
||||||
|
*/
|
||||||
|
fun merge(logger: Logger) {
|
||||||
|
with(context.bytecodeContext.classes) {
|
||||||
|
for (integrations in integrations) {
|
||||||
|
callback?.let { it(integrations) }
|
||||||
|
|
||||||
|
for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile(
|
||||||
|
true,
|
||||||
|
integrations,
|
||||||
|
NAMER,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
).classes) {
|
||||||
|
val type = classDef.type
|
||||||
|
|
||||||
|
val result = classes.findIndexed { it.type == type }
|
||||||
|
if (result == null) {
|
||||||
|
logger.trace("Merging type $type")
|
||||||
|
classes.add(classDef)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val (existingClass, existingClassIndex) = result
|
||||||
|
|
||||||
|
logger.trace("Type $type exists. Adding missing methods and fields.")
|
||||||
|
|
||||||
|
existingClass.merge(classDef, context, logger).let { mergedClass ->
|
||||||
|
if (mergedClass !== existingClass) // referential equality check
|
||||||
|
classes[existingClassIndex] = mergedClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/kotlin/app/revanced/patcher/PatcherOptions.kt
Normal file
21
src/main/kotlin/app/revanced/patcher/PatcherOptions.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
16
src/main/kotlin/app/revanced/patcher/PatcherResult.kt
Normal file
16
src/main/kotlin/app/revanced/patcher/PatcherResult.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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?
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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> = [],
|
||||||
|
)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package app.revanced.patcher.annotation
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to name a [Patch].
|
||||||
|
* @param name A suggestive name for the [Patch].
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Name(
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to describe a [Patch].
|
||||||
|
* @param description A description for the [Patch].
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Description(
|
||||||
|
val description: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to version a [Patch].
|
||||||
|
* @param version The version of a [Patch].
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Version(
|
||||||
|
val version: String,
|
||||||
|
)
|
||||||
170
src/main/kotlin/app/revanced/patcher/data/Context.kt
Normal file
170
src/main/kotlin/app/revanced/patcher/data/Context.kt
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package app.revanced.patcher.data
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.ProxyBackedClassList
|
||||||
|
import app.revanced.patcher.util.method.MethodWalker
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
import javax.xml.transform.TransformerFactory
|
||||||
|
import javax.xml.transform.dom.DOMSource
|
||||||
|
import javax.xml.transform.stream.StreamResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext].
|
||||||
|
*/
|
||||||
|
|
||||||
|
sealed interface Context
|
||||||
|
|
||||||
|
class BytecodeContext internal constructor(classes: MutableList<ClassDef>) : Context {
|
||||||
|
/**
|
||||||
|
* The list of classes.
|
||||||
|
*/
|
||||||
|
val classes = ProxyBackedClassList(classes)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a class by a given class name.
|
||||||
|
*
|
||||||
|
* @param className The name of the class.
|
||||||
|
* @return A proxy for the first class that matches the class name.
|
||||||
|
*/
|
||||||
|
fun findClass(className: String) = findClass { it.type.contains(className) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a class by a given predicate.
|
||||||
|
*
|
||||||
|
* @param predicate A predicate to match the class.
|
||||||
|
* @return A proxy for the first class that matches the predicate.
|
||||||
|
*/
|
||||||
|
fun findClass(predicate: (ClassDef) -> Boolean) =
|
||||||
|
// if we already proxied the class matching the predicate...
|
||||||
|
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
|
||||||
|
// else resolve the class to a proxy and return it, if the predicate is matching a class
|
||||||
|
classes.find(predicate)?.let { proxy(it) }
|
||||||
|
|
||||||
|
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
|
||||||
|
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
|
||||||
|
if (proxy == null) {
|
||||||
|
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
|
||||||
|
this.classes.add(proxy)
|
||||||
|
}
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
||||||
|
*
|
||||||
|
* @param startMethod The method to start at.
|
||||||
|
* @return A [MethodWalker] instance.
|
||||||
|
*/
|
||||||
|
fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker {
|
||||||
|
return MethodWalker(this, startMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
|
||||||
|
for ((index, element) in this.withIndex()) {
|
||||||
|
if (predicate(element)) {
|
||||||
|
return element to index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable<File> {
|
||||||
|
val xmlEditor = XmlFileHolder()
|
||||||
|
|
||||||
|
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
|
||||||
|
|
||||||
|
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
|
||||||
|
|
||||||
|
inner class XmlFileHolder {
|
||||||
|
operator fun get(inputStream: InputStream) =
|
||||||
|
DomFileEditor(inputStream)
|
||||||
|
|
||||||
|
operator fun get(path: String): DomFileEditor {
|
||||||
|
return DomFileEditor(this@ResourceContext[path])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for a file that can be edited as a dom document.
|
||||||
|
*
|
||||||
|
* This constructor does not check for locks to the file when writing.
|
||||||
|
* Use the secondary constructor.
|
||||||
|
*
|
||||||
|
* @param inputStream the input stream to read the xml file from.
|
||||||
|
* @param outputStream the output stream to write the xml file to. If null, the file will be read only.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class DomFileEditor internal constructor(
|
||||||
|
private val inputStream: InputStream,
|
||||||
|
private val outputStream: Lazy<OutputStream>? = null,
|
||||||
|
) : Closeable {
|
||||||
|
// path to the xml file to unlock the resource when closing the editor
|
||||||
|
private var filePath: String? = null
|
||||||
|
private var closed: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The document of the xml file
|
||||||
|
*/
|
||||||
|
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
|
||||||
|
.also(Document::normalize)
|
||||||
|
|
||||||
|
|
||||||
|
// lazily open an output stream
|
||||||
|
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
|
||||||
|
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
|
||||||
|
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
|
||||||
|
// increase the lock
|
||||||
|
locks.merge(file.path, 1, Integer::sum)
|
||||||
|
filePath = file.path
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the editor. Write backs and decreases the lock count.
|
||||||
|
*
|
||||||
|
* Will not write back to the file if the file is still locked.
|
||||||
|
*/
|
||||||
|
override fun close() {
|
||||||
|
if (closed) return
|
||||||
|
|
||||||
|
inputStream.close()
|
||||||
|
|
||||||
|
// if the output stream is not null, do not close it
|
||||||
|
outputStream?.let {
|
||||||
|
// prevent writing to same file, if it is being locked
|
||||||
|
// isLocked will be false if the editor was created through a stream
|
||||||
|
val isLocked = filePath?.let { path ->
|
||||||
|
val isLocked = locks[path]!! > 1
|
||||||
|
// decrease the lock count if the editor was opened for a file
|
||||||
|
locks.merge(path, -1, Integer::sum)
|
||||||
|
isLocked
|
||||||
|
} ?: false
|
||||||
|
|
||||||
|
// if unlocked, write back to the file
|
||||||
|
if (!isLocked) {
|
||||||
|
it.value.use { stream ->
|
||||||
|
val result = StreamResult(stream)
|
||||||
|
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.value.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
// map of concurrent open files
|
||||||
|
val locks = mutableMapOf<String, Int>()
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main/kotlin/app/revanced/patcher/data/PackageMetadata.kt
Normal file
13
src/main/kotlin/app/revanced/patcher/data/PackageMetadata.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.patcher.data
|
||||||
|
|
||||||
|
import brut.androlib.meta.MetaInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about a package.
|
||||||
|
*/
|
||||||
|
class PackageMetadata {
|
||||||
|
lateinit var packageName: String
|
||||||
|
lateinit var packageVersion: String
|
||||||
|
|
||||||
|
internal val metaInfo: MetaInfo = MetaInfo()
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
internal object AnnotationExtensions {
|
||||||
|
/**
|
||||||
|
* Recursively find a given annotation on a class.
|
||||||
|
*
|
||||||
|
* @param targetAnnotation The annotation to find.
|
||||||
|
* @return The annotation.
|
||||||
|
*/
|
||||||
|
fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: KClass<T>): T? {
|
||||||
|
fun <T : Annotation> Class<*>.findAnnotationRecursively(
|
||||||
|
targetAnnotation: Class<T>, traversed: MutableSet<Annotation>
|
||||||
|
): T? {
|
||||||
|
val found = this.annotations.firstOrNull { it.annotationClass.java.name == targetAnnotation.name }
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST") if (found != null) return found as T
|
||||||
|
|
||||||
|
for (annotation in this.annotations) {
|
||||||
|
if (traversed.contains(annotation)) continue
|
||||||
|
traversed.add(annotation)
|
||||||
|
|
||||||
|
return (annotation.annotationClass.java.findAnnotationRecursively(targetAnnotation, traversed))
|
||||||
|
?: continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a bitwise OR operation between two [AccessFlags].
|
||||||
|
*
|
||||||
|
* @param other The other [AccessFlags] to perform the operation with.
|
||||||
|
*/
|
||||||
|
infix fun AccessFlags.or(other: AccessFlags) = value or other.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a bitwise OR operation between an [AccessFlags] and an [Int].
|
||||||
|
*
|
||||||
|
* @param other The [Int] to perform the operation with.
|
||||||
|
*/
|
||||||
|
infix fun Int.or(other: AccessFlags) = this or other.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a bitwise OR operation between an [Int] and an [AccessFlags].
|
||||||
|
*
|
||||||
|
* @param other The [AccessFlags] to perform the operation with.
|
||||||
|
*/
|
||||||
|
infix fun AccessFlags.or(other: Int) = value or other
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
|
import app.revanced.patcher.util.smali.ExternalLabel
|
||||||
|
import app.revanced.patcher.util.smali.toInstruction
|
||||||
|
import app.revanced.patcher.util.smali.toInstructions
|
||||||
|
import org.jf.dexlib2.builder.BuilderInstruction
|
||||||
|
import org.jf.dexlib2.builder.BuilderOffsetInstruction
|
||||||
|
import org.jf.dexlib2.builder.Label
|
||||||
|
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.builder.instruction.*
|
||||||
|
import org.jf.dexlib2.iface.instruction.Instruction
|
||||||
|
|
||||||
|
object InstructionExtensions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 { this.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>) {
|
||||||
|
// Remove the instructions at the given index.
|
||||||
|
removeInstructions(index, instructions.size)
|
||||||
|
|
||||||
|
// Add the instructions at the given index.
|
||||||
|
addInstructions(index, instructions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.toInstruction(this))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an instruction to a method.
|
||||||
|
*
|
||||||
|
* @param smaliInstructions The instruction to add.
|
||||||
|
*/
|
||||||
|
fun MutableMethod.addInstruction(smaliInstructions: String) =
|
||||||
|
implementation!!.addInstruction(smaliInstructions.toInstruction(this))
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new label for the instruction
|
||||||
|
* and replaces 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.toInstruction(this))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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.
|
||||||
|
*/
|
||||||
|
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = implementation!!.getInstruction(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> MutableMethod.getInstruction(index: Int): T = implementation!!.getInstruction<T>(index)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||||
|
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
|
||||||
|
object MethodFingerprintExtensions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of a [MethodFingerprint].
|
||||||
|
*/
|
||||||
|
val MethodFingerprint.name: String
|
||||||
|
get() = this.javaClass.simpleName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint].
|
||||||
|
*/
|
||||||
|
val MethodFingerprint.fuzzyPatternScanMethod
|
||||||
|
get() = javaClass.findAnnotationRecursively(FuzzyPatternScanMethod::class)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
|
import app.revanced.patcher.annotation.Compatibility
|
||||||
|
import app.revanced.patcher.annotation.Description
|
||||||
|
import app.revanced.patcher.annotation.Name
|
||||||
|
import app.revanced.patcher.annotation.Version
|
||||||
|
import app.revanced.patcher.data.Context
|
||||||
|
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||||
|
import app.revanced.patcher.patch.OptionsContainer
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.PatchOptions
|
||||||
|
import app.revanced.patcher.patch.annotations.DependsOn
|
||||||
|
import app.revanced.patcher.patch.annotations.RequiresIntegrations
|
||||||
|
import kotlin.reflect.KVisibility
|
||||||
|
import kotlin.reflect.full.companionObject
|
||||||
|
import kotlin.reflect.full.companionObjectInstance
|
||||||
|
|
||||||
|
object PatchExtensions {
|
||||||
|
/**
|
||||||
|
* The name of a [Patch].
|
||||||
|
*/
|
||||||
|
val Class<out Patch<Context>>.patchName: String
|
||||||
|
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of a [Patch].
|
||||||
|
*/
|
||||||
|
val Class<out Patch<Context>>.version
|
||||||
|
get() = findAnnotationRecursively(Version::class)?.version
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather or not a [Patch] should be included.
|
||||||
|
*/
|
||||||
|
val Class<out Patch<Context>>.include
|
||||||
|
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The description of a [Patch].
|
||||||
|
*/
|
||||||
|
val Class<out Patch<Context>>.description
|
||||||
|
get() = findAnnotationRecursively(Description::class)?.description
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dependencies of a [Patch].
|
||||||
|
*/
|
||||||
|
val Class<out Patch<Context>>.dependencies
|
||||||
|
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The packages a [Patch] is compatible with.
|
||||||
|
*/
|
||||||
|
val Class<out Patch<Context>>.compatiblePackages
|
||||||
|
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather or not a [Patch] requires integrations.
|
||||||
|
*/
|
||||||
|
internal val Class<out Patch<Context>>.requiresIntegrations
|
||||||
|
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options of a [Patch].
|
||||||
|
*/
|
||||||
|
val Class<out Patch<Context>>.options: PatchOptions?
|
||||||
|
get() = kotlin.companionObject?.let { cl ->
|
||||||
|
if (cl.visibility != KVisibility.PUBLIC) return null
|
||||||
|
kotlin.companionObjectInstance?.let {
|
||||||
|
(it as? OptionsContainer)?.options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patcher.fingerprint
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ReVanced fingerprint.
|
||||||
|
* Can be a [MethodFingerprint].
|
||||||
|
*/
|
||||||
|
interface Fingerprint
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package app.revanced.patcher.fingerprint.method.annotation
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
|
||||||
|
* @param threshold if [threshold] or more of the opcodes do not match, skip.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class FuzzyPatternScanMethod(
|
||||||
|
val threshold: Int = 1
|
||||||
|
)
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
package app.revanced.patcher.fingerprint.method.impl
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
|
||||||
|
import app.revanced.patcher.fingerprint.Fingerprint
|
||||||
|
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
||||||
|
import app.revanced.patcher.util.proxy.ClassProxy
|
||||||
|
import org.jf.dexlib2.Opcode
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
import org.jf.dexlib2.iface.instruction.Instruction
|
||||||
|
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
|
import org.jf.dexlib2.iface.reference.StringReference
|
||||||
|
import org.jf.dexlib2.util.MethodUtil
|
||||||
|
|
||||||
|
private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch
|
||||||
|
private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the [MethodFingerprint] for a method.
|
||||||
|
* @param returnType The return type of the method.
|
||||||
|
* @param accessFlags The access flags of the method.
|
||||||
|
* @param parameters The parameters of the method.
|
||||||
|
* @param opcodes The list of opcodes of the method.
|
||||||
|
* @param strings A list of strings which a method contains.
|
||||||
|
* @param customFingerprint A custom condition for this fingerprint.
|
||||||
|
* A `null` opcode is equals to an unknown opcode.
|
||||||
|
*/
|
||||||
|
abstract class MethodFingerprint(
|
||||||
|
internal val returnType: String? = null,
|
||||||
|
internal val accessFlags: Int? = null,
|
||||||
|
internal val parameters: Iterable<String>? = null,
|
||||||
|
internal val opcodes: Iterable<Opcode?>? = null,
|
||||||
|
internal val strings: Iterable<String>? = null,
|
||||||
|
internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null
|
||||||
|
) : Fingerprint {
|
||||||
|
/**
|
||||||
|
* The result of the [MethodFingerprint].
|
||||||
|
*/
|
||||||
|
var result: MethodFingerprintResult? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
|
||||||
|
*
|
||||||
|
* @param classes The classes on which to resolve the [MethodFingerprint] in.
|
||||||
|
* @param context The [BytecodeContext] to host proxies.
|
||||||
|
* @return True if the resolution was successful, false otherwise.
|
||||||
|
*/
|
||||||
|
fun Iterable<MethodFingerprint>.resolve(context: BytecodeContext, classes: Iterable<ClassDef>) {
|
||||||
|
for (fingerprint in this) // For each fingerprint
|
||||||
|
classes@ for (classDef in classes) // search through all classes for the fingerprint
|
||||||
|
if (fingerprint.resolve(context, classDef))
|
||||||
|
break@classes // if the resolution succeeded, continue with the next fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a [MethodFingerprint] against a [ClassDef].
|
||||||
|
*
|
||||||
|
* @param forClass The class on which to resolve the [MethodFingerprint] in.
|
||||||
|
* @param context The [BytecodeContext] to host proxies.
|
||||||
|
* @return True if the resolution was successful, false otherwise.
|
||||||
|
*/
|
||||||
|
fun MethodFingerprint.resolve(context: BytecodeContext, forClass: ClassDef): Boolean {
|
||||||
|
for (method in forClass.methods)
|
||||||
|
if (this.resolve(context, method, forClass))
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a [MethodFingerprint] against a [Method].
|
||||||
|
*
|
||||||
|
* @param method The class on which to resolve the [MethodFingerprint] in.
|
||||||
|
* @param forClass The class on which to resolve the [MethodFingerprint].
|
||||||
|
* @param context The [BytecodeContext] to host proxies.
|
||||||
|
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
|
||||||
|
*/
|
||||||
|
fun MethodFingerprint.resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean {
|
||||||
|
val methodFingerprint = this
|
||||||
|
|
||||||
|
if (methodFingerprint.result != null) return true
|
||||||
|
|
||||||
|
if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType))
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (methodFingerprint.parameters != null && !parametersEqual(
|
||||||
|
methodFingerprint.parameters, // TODO: parseParameters()
|
||||||
|
method.parameterTypes
|
||||||
|
)
|
||||||
|
) return false
|
||||||
|
|
||||||
|
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||||
|
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass))
|
||||||
|
return false
|
||||||
|
|
||||||
|
val stringsScanResult: StringsScanResult? =
|
||||||
|
if (methodFingerprint.strings != null) {
|
||||||
|
StringsScanResult(
|
||||||
|
buildList {
|
||||||
|
val implementation = method.implementation ?: return false
|
||||||
|
|
||||||
|
val stringsList = methodFingerprint.strings.toMutableList()
|
||||||
|
|
||||||
|
implementation.instructions.forEachIndexed { instructionIndex, instruction ->
|
||||||
|
if (
|
||||||
|
instruction.opcode != Opcode.CONST_STRING &&
|
||||||
|
instruction.opcode != Opcode.CONST_STRING_JUMBO
|
||||||
|
) return@forEachIndexed
|
||||||
|
|
||||||
|
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
|
||||||
|
val index = stringsList.indexOfFirst(string::contains)
|
||||||
|
if (index == -1) return@forEachIndexed
|
||||||
|
|
||||||
|
add(
|
||||||
|
StringMatch(
|
||||||
|
string,
|
||||||
|
instructionIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stringsList.removeAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringsList.isNotEmpty()) return false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
|
||||||
|
val patternScanResult = if (methodFingerprint.opcodes != null) {
|
||||||
|
method.implementation?.instructions ?: return false
|
||||||
|
|
||||||
|
method.patternScan(methodFingerprint) ?: return false
|
||||||
|
} else null
|
||||||
|
|
||||||
|
methodFingerprint.result = MethodFingerprintResult(
|
||||||
|
method,
|
||||||
|
forClass,
|
||||||
|
MethodFingerprintResult.MethodFingerprintScanResult(
|
||||||
|
patternScanResult,
|
||||||
|
stringsScanResult
|
||||||
|
),
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Method.patternScan(
|
||||||
|
fingerprint: MethodFingerprint
|
||||||
|
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
|
||||||
|
val instructions = this.implementation!!.instructions
|
||||||
|
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
|
||||||
|
|
||||||
|
val pattern = fingerprint.opcodes!!
|
||||||
|
val instructionLength = instructions.count()
|
||||||
|
val patternLength = pattern.count()
|
||||||
|
|
||||||
|
for (index in 0 until instructionLength) {
|
||||||
|
var patternIndex = 0
|
||||||
|
var threshold = fingerprintFuzzyPatternScanThreshold
|
||||||
|
|
||||||
|
while (index + patternIndex < instructionLength) {
|
||||||
|
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
|
||||||
|
val patternOpcode = pattern.elementAt(patternIndex)
|
||||||
|
|
||||||
|
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
|
||||||
|
// reaching maximum threshold (0) means,
|
||||||
|
// the pattern does not match to the current instructions
|
||||||
|
if (threshold-- == 0) break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patternIndex < patternLength - 1) {
|
||||||
|
// if the entire pattern has not been scanned yet
|
||||||
|
// continue the scan
|
||||||
|
patternIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
|
||||||
|
val result =
|
||||||
|
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
|
||||||
|
index,
|
||||||
|
index + patternIndex
|
||||||
|
)
|
||||||
|
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
|
||||||
|
result.warnings = result.createWarnings(pattern, instructions)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.createWarnings(
|
||||||
|
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
|
||||||
|
) = buildList {
|
||||||
|
for ((patternIndex, instructionIndex) in (this@createWarnings.startIndex until this@createWarnings.endIndex).withIndex()) {
|
||||||
|
val originalOpcode = instructions.elementAt(instructionIndex).opcode
|
||||||
|
val patternOpcode = pattern.elementAt(patternIndex)
|
||||||
|
|
||||||
|
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
|
||||||
|
|
||||||
|
this.add(
|
||||||
|
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
|
||||||
|
originalOpcode,
|
||||||
|
patternOpcode,
|
||||||
|
instructionIndex,
|
||||||
|
patternIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the result of a [MethodFingerprintResult].
|
||||||
|
*
|
||||||
|
* @param method The matching method.
|
||||||
|
* @param classDef The [ClassDef] that contains the matching [method].
|
||||||
|
* @param scanResult The result of scanning for the [MethodFingerprint].
|
||||||
|
* @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
|
||||||
|
*/
|
||||||
|
data class MethodFingerprintResult(
|
||||||
|
val method: Method,
|
||||||
|
val classDef: ClassDef,
|
||||||
|
val scanResult: MethodFingerprintScanResult,
|
||||||
|
internal val context: BytecodeContext
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Returns a mutable clone of [classDef]
|
||||||
|
*
|
||||||
|
* Please note, this method allocates a [ClassProxy].
|
||||||
|
* Use [classDef] where possible.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
val mutableClass by lazy { context.proxy(classDef).mutableClass }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a mutable clone of [method]
|
||||||
|
*
|
||||||
|
* Please note, this method allocates a [ClassProxy].
|
||||||
|
* Use [method] where possible.
|
||||||
|
*/
|
||||||
|
val mutableMethod by lazy {
|
||||||
|
mutableClass.methods.first {
|
||||||
|
MethodUtil.methodSignaturesMatch(it, this.method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of scanning on the [MethodFingerprint].
|
||||||
|
* @param patternScanResult The result of the pattern scan.
|
||||||
|
* @param stringsScanResult The result of the string scan.
|
||||||
|
*/
|
||||||
|
data class MethodFingerprintScanResult(
|
||||||
|
val patternScanResult: PatternScanResult?,
|
||||||
|
val stringsScanResult: StringsScanResult?
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* The result of scanning strings on the [MethodFingerprint].
|
||||||
|
* @param matches The list of strings that were matched.
|
||||||
|
*/
|
||||||
|
data class StringsScanResult(val matches: List<StringMatch>) {
|
||||||
|
/**
|
||||||
|
* Represents a match for a string at an index.
|
||||||
|
* @param string The string that was matched.
|
||||||
|
* @param index The index of the string.
|
||||||
|
*/
|
||||||
|
data class StringMatch(val string: String, val index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a pattern scan.
|
||||||
|
* @param startIndex The start index of the instructions where to which this pattern matches.
|
||||||
|
* @param endIndex The end index of the instructions where to which this pattern matches.
|
||||||
|
* @param warnings A list of warnings considering this [PatternScanResult].
|
||||||
|
*/
|
||||||
|
data class PatternScanResult(
|
||||||
|
val startIndex: Int,
|
||||||
|
val endIndex: Int,
|
||||||
|
var warnings: List<Warning>? = null
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Represents warnings of the pattern scan.
|
||||||
|
* @param correctOpcode The opcode the instruction list has.
|
||||||
|
* @param wrongOpcode The opcode the pattern list of the signature currently has.
|
||||||
|
* @param instructionIndex The index of the opcode relative to the instruction list.
|
||||||
|
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
|
||||||
|
*/
|
||||||
|
data class Warning(
|
||||||
|
val correctOpcode: Opcode,
|
||||||
|
val wrongOpcode: Opcode,
|
||||||
|
val instructionIndex: Int,
|
||||||
|
val patternIndex: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/main/kotlin/app/revanced/patcher/logging/Logger.kt
Normal file
8
src/main/kotlin/app/revanced/patcher/logging/Logger.kt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package app.revanced.patcher.logging
|
||||||
|
|
||||||
|
interface Logger {
|
||||||
|
fun error(msg: String) {}
|
||||||
|
fun warn(msg: String) {}
|
||||||
|
fun info(msg: String) {}
|
||||||
|
fun trace(msg: String) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package app.revanced.patcher.logging.impl
|
||||||
|
|
||||||
|
import app.revanced.patcher.logging.Logger
|
||||||
|
|
||||||
|
object NopLogger : Logger
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container for patch options.
|
||||||
|
*/
|
||||||
|
abstract class OptionsContainer {
|
||||||
|
/**
|
||||||
|
* A list of [PatchOption]s.
|
||||||
|
* @see PatchOptions
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
val options = PatchOptions()
|
||||||
|
|
||||||
|
protected fun <T> option(opt: PatchOption<T>): PatchOption<T> {
|
||||||
|
options.register(opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/kotlin/app/revanced/patcher/patch/Patch.kt
Normal file
37
src/main/kotlin/app/revanced/patcher/patch/Patch.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.data.Context
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
import java.io.Closeable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ReVanced patch.
|
||||||
|
*
|
||||||
|
* If it implements [Closeable], it will be closed after all patches have been executed.
|
||||||
|
* Closing will be done in reverse execution order.
|
||||||
|
*/
|
||||||
|
sealed interface Patch<out T : Context> {
|
||||||
|
/**
|
||||||
|
* The main function of the [Patch] which the patcher will call.
|
||||||
|
*
|
||||||
|
* @param context The [Context] the patch will work on.
|
||||||
|
* @return The result of executing the patch.
|
||||||
|
*/
|
||||||
|
fun execute(context: @UnsafeVariance T): PatchResult
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource patch for the Patcher.
|
||||||
|
*/
|
||||||
|
interface ResourcePatch : Patch<ResourceContext>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytecode patch for the Patcher.
|
||||||
|
*
|
||||||
|
* @param fingerprints A list of [MethodFingerprint] this patch relies on.
|
||||||
|
*/
|
||||||
|
abstract class BytecodePatch(
|
||||||
|
internal val fingerprints: Iterable<MethodFingerprint>? = null
|
||||||
|
) : Patch<BytecodeContext>
|
||||||
248
src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt
Normal file
248
src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
@file:Suppress("CanBeParameter", "MemberVisibilityCanBePrivate", "UNCHECKED_CAST")
|
||||||
|
|
||||||
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
class NoSuchOptionException(val option: String) : Exception("No such option: $option")
|
||||||
|
class IllegalValueException(val value: Any?) : Exception("Illegal value: $value")
|
||||||
|
class InvalidTypeException(val got: String, val expected: String) :
|
||||||
|
Exception("Invalid option value type: $got, expected $expected")
|
||||||
|
|
||||||
|
object RequirementNotMetException : Exception("null was passed into an option that requires a value")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A registry for an array of [PatchOption]s.
|
||||||
|
* @param options An array of [PatchOption]s.
|
||||||
|
*/
|
||||||
|
class PatchOptions(vararg options: PatchOption<*>) : Iterable<PatchOption<*>> {
|
||||||
|
private val register = mutableMapOf<String, PatchOption<*>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
options.forEach { register(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun register(option: PatchOption<*>) {
|
||||||
|
if (register.containsKey(option.key)) {
|
||||||
|
throw IllegalStateException("Multiple options found with the same key")
|
||||||
|
}
|
||||||
|
register[option.key] = option
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a [PatchOption] by its key.
|
||||||
|
* @param key The key of the [PatchOption].
|
||||||
|
*/
|
||||||
|
@JvmName("getUntyped")
|
||||||
|
operator fun get(key: String) = register[key] ?: throw NoSuchOptionException(key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a [PatchOption] by its key.
|
||||||
|
* @param key The key of the [PatchOption].
|
||||||
|
*/
|
||||||
|
inline operator fun <reified T> get(key: String): PatchOption<T> {
|
||||||
|
val opt = get(key)
|
||||||
|
if (opt.value !is T) throw InvalidTypeException(
|
||||||
|
opt.value?.let { it::class.java.canonicalName } ?: "null",
|
||||||
|
T::class.java.canonicalName
|
||||||
|
)
|
||||||
|
return opt as PatchOption<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of a [PatchOption].
|
||||||
|
* @param key The key of the [PatchOption].
|
||||||
|
* @param value The value you want it to be.
|
||||||
|
* Please note that using the wrong value type results in a runtime error.
|
||||||
|
*/
|
||||||
|
inline operator fun <reified T> set(key: String, value: T) {
|
||||||
|
val opt = get<T>(key)
|
||||||
|
if (opt.value !is T) throw InvalidTypeException(
|
||||||
|
T::class.java.canonicalName,
|
||||||
|
opt.value?.let { it::class.java.canonicalName } ?: "null"
|
||||||
|
)
|
||||||
|
opt.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value of a [PatchOption] to `null`.
|
||||||
|
* @param key The key of the [PatchOption].
|
||||||
|
*/
|
||||||
|
fun nullify(key: String) {
|
||||||
|
get(key).value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun iterator() = register.values.iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Patch] option.
|
||||||
|
* @param key Unique identifier of the option. Example: _`settings.microg.enabled`_
|
||||||
|
* @param default The default value of the option.
|
||||||
|
* @param title A human-readable title of the option. Example: _MicroG Settings_
|
||||||
|
* @param description A human-readable description of the option. Example: _Settings integration for MicroG._
|
||||||
|
* @param required Whether the option is required.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
sealed class PatchOption<T>(
|
||||||
|
val key: String,
|
||||||
|
default: T?,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val required: Boolean,
|
||||||
|
val validator: (T?) -> Boolean
|
||||||
|
) {
|
||||||
|
var value: T? = default
|
||||||
|
get() {
|
||||||
|
if (field == null && required) {
|
||||||
|
throw RequirementNotMetException
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (value == null && required) {
|
||||||
|
throw RequirementNotMetException
|
||||||
|
}
|
||||||
|
if (!validator(value)) {
|
||||||
|
throw IllegalValueException(value)
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of the option.
|
||||||
|
* Please note that using the wrong value type results in a runtime error.
|
||||||
|
*/
|
||||||
|
@JvmName("getValueTyped")
|
||||||
|
inline operator fun <reified V> getValue(thisRef: Nothing?, property: KProperty<*>): V? {
|
||||||
|
if (value !is V?) throw InvalidTypeException(
|
||||||
|
V::class.java.canonicalName,
|
||||||
|
value?.let { it::class.java.canonicalName } ?: "null"
|
||||||
|
)
|
||||||
|
return value as? V?
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of the option.
|
||||||
|
* Please note that using the wrong value type results in a runtime error.
|
||||||
|
*/
|
||||||
|
@JvmName("setValueTyped")
|
||||||
|
inline operator fun <reified V> setValue(thisRef: Nothing?, property: KProperty<*>, new: V) {
|
||||||
|
if (value !is V) throw InvalidTypeException(
|
||||||
|
V::class.java.canonicalName,
|
||||||
|
value?.let { it::class.java.canonicalName } ?: "null"
|
||||||
|
)
|
||||||
|
value = new as T
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, new: T?) {
|
||||||
|
value = new
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchOption] representing a [String].
|
||||||
|
* @see PatchOption
|
||||||
|
*/
|
||||||
|
class StringOption(
|
||||||
|
key: String,
|
||||||
|
default: String?,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (String?) -> Boolean = { true }
|
||||||
|
) : PatchOption<String>(
|
||||||
|
key, default, title, description, required, validator
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchOption] representing a [Boolean].
|
||||||
|
* @see PatchOption
|
||||||
|
*/
|
||||||
|
class BooleanOption(
|
||||||
|
key: String,
|
||||||
|
default: Boolean?,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (Boolean?) -> Boolean = { true }
|
||||||
|
) : PatchOption<Boolean>(
|
||||||
|
key, default, title, description, required, validator
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchOption] with a list of allowed options.
|
||||||
|
* @param options A list of allowed options for the [ListOption].
|
||||||
|
* @see PatchOption
|
||||||
|
*/
|
||||||
|
sealed class ListOption<E>(
|
||||||
|
key: String,
|
||||||
|
default: E?,
|
||||||
|
val options: Iterable<E>,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (E?) -> Boolean = { true }
|
||||||
|
) : PatchOption<E>(
|
||||||
|
key, default, title, description, required, {
|
||||||
|
(it?.let { it in options } ?: true) && validator(it)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
if (default != null && default !in options) {
|
||||||
|
throw IllegalStateException("Default option must be an allowed option")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [ListOption] of type [String].
|
||||||
|
* @see ListOption
|
||||||
|
*/
|
||||||
|
class StringListOption(
|
||||||
|
key: String,
|
||||||
|
default: String?,
|
||||||
|
options: Iterable<String>,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (String?) -> Boolean = { true }
|
||||||
|
) : ListOption<String>(
|
||||||
|
key, default, options, title, description, required, validator
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [ListOption] of type [Int].
|
||||||
|
* @see ListOption
|
||||||
|
*/
|
||||||
|
class IntListOption(
|
||||||
|
key: String,
|
||||||
|
default: Int?,
|
||||||
|
options: Iterable<Int>,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (Int?) -> Boolean = { true }
|
||||||
|
) : ListOption<Int>(
|
||||||
|
key, default, options, title, description, required, validator
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchOption] representing a [Path], backed by a [String].
|
||||||
|
* The validator passes a [String], if you need a [Path] you will have to convert it yourself.
|
||||||
|
* @see PatchOption
|
||||||
|
*/
|
||||||
|
class PathOption(
|
||||||
|
key: String,
|
||||||
|
default: Path?,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (String?) -> Boolean = { true }
|
||||||
|
) : PatchOption<String>(
|
||||||
|
key, default?.pathString, title, description, required, validator
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package net.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
interface PatchResult {
|
interface PatchResult {
|
||||||
fun error(): PatchResultError? {
|
fun error(): PatchResultError? {
|
||||||
@@ -24,10 +24,12 @@ interface PatchResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PatchResultError(private val errorMessage: String) : PatchResult {
|
class PatchResultError(
|
||||||
fun errorMessage(): String {
|
errorMessage: String?, cause: Exception?
|
||||||
return errorMessage
|
) : Exception(errorMessage, cause), PatchResult {
|
||||||
}
|
constructor(errorMessage: String) : this(errorMessage, null)
|
||||||
|
constructor(cause: Exception) : this(cause.message, cause)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PatchResultSuccess : PatchResult
|
class PatchResultSuccess : PatchResult
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package app.revanced.patcher.patch.annotations
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.Context
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to mark a class as a patch.
|
||||||
|
* @param include If false, the patch should be treated as optional by default.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Patch(val include: Boolean = true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation for dependencies of [Patch]es.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class DependsOn(
|
||||||
|
val dependencies: Array<KClass<out Patch<Context>>> = []
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to mark [Patch]es which depend on integrations.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class RequiresIntegrations // required because integrations are decoupled from patches
|
||||||
214
src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt
Normal file
214
src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import app.revanced.patcher.PatcherContext
|
||||||
|
import app.revanced.patcher.extensions.or
|
||||||
|
import app.revanced.patcher.logging.Logger
|
||||||
|
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
|
||||||
|
import app.revanced.patcher.util.ClassMerger.Utils.filterAny
|
||||||
|
import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
|
||||||
|
import app.revanced.patcher.util.ClassMerger.Utils.isPublic
|
||||||
|
import app.revanced.patcher.util.ClassMerger.Utils.toPublic
|
||||||
|
import app.revanced.patcher.util.TypeUtil.traverseClassHierarchy
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableField
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import org.jf.dexlib2.util.MethodUtil
|
||||||
|
import kotlin.reflect.KFunction2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimental class to merge a [ClassDef] with another.
|
||||||
|
* Note: This will not consider method implementations or if the class is missing a superclass or interfaces.
|
||||||
|
*/
|
||||||
|
internal object ClassMerger {
|
||||||
|
/**
|
||||||
|
* Merge a class with [otherClass].
|
||||||
|
*
|
||||||
|
* @param otherClass The class to merge with
|
||||||
|
* @param context The context to traverse the class hierarchy in.
|
||||||
|
* @param logger A logger.
|
||||||
|
*/
|
||||||
|
fun ClassDef.merge(otherClass: ClassDef, context: PatcherContext, logger: Logger? = null) = this
|
||||||
|
//.fixFieldAccess(otherClass, logger)
|
||||||
|
//.fixMethodAccess(otherClass, logger)
|
||||||
|
.addMissingFields(otherClass, logger)
|
||||||
|
.addMissingMethods(otherClass, logger)
|
||||||
|
.publicize(otherClass, context, logger)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add methods which are missing but existing in [fromClass].
|
||||||
|
*
|
||||||
|
* @param fromClass The class to add missing methods from.
|
||||||
|
* @param logger A logger.
|
||||||
|
*/
|
||||||
|
private fun ClassDef.addMissingMethods(fromClass: ClassDef, logger: Logger? = null): ClassDef {
|
||||||
|
val missingMethods = fromClass.methods.let { fromMethods ->
|
||||||
|
methods.filterNot { method ->
|
||||||
|
fromMethods.any { fromMethod ->
|
||||||
|
MethodUtil.methodSignaturesMatch(fromMethod, method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingMethods.isEmpty()) return this
|
||||||
|
|
||||||
|
logger?.trace("Found ${missingMethods.size} missing methods")
|
||||||
|
|
||||||
|
return asMutableClass().apply {
|
||||||
|
methods.addAll(missingMethods.map { it.toMutable() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add fields which are missing but existing in [fromClass].
|
||||||
|
*
|
||||||
|
* @param fromClass The class to add missing fields from.
|
||||||
|
* @param logger A logger.
|
||||||
|
*/
|
||||||
|
private fun ClassDef.addMissingFields(fromClass: ClassDef, logger: Logger? = null): ClassDef {
|
||||||
|
val missingFields = fields.filterNotAny(fromClass.fields) { field, fromField ->
|
||||||
|
fromField.name == field.name
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingFields.isEmpty()) return this
|
||||||
|
|
||||||
|
logger?.trace("Found ${missingFields.size} missing fields")
|
||||||
|
|
||||||
|
return asMutableClass().apply {
|
||||||
|
fields.addAll(missingFields.map { it.toMutable() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a class and its super class public recursively.
|
||||||
|
* @param reference The class to check the [AccessFlags] of.
|
||||||
|
* @param context The context to traverse the class hierarchy in.
|
||||||
|
* @param logger A logger.
|
||||||
|
*/
|
||||||
|
private fun ClassDef.publicize(reference: ClassDef, context: PatcherContext, logger: Logger? = null) =
|
||||||
|
if (reference.accessFlags.isPublic() && !accessFlags.isPublic())
|
||||||
|
this.asMutableClass().apply {
|
||||||
|
context.bytecodeContext.traverseClassHierarchy(this) {
|
||||||
|
if (accessFlags.isPublic()) return@traverseClassHierarchy
|
||||||
|
|
||||||
|
logger?.trace("Publicizing ${this.type}")
|
||||||
|
|
||||||
|
accessFlags = accessFlags.toPublic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else this
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publicize fields if they are public in [reference].
|
||||||
|
*
|
||||||
|
* @param reference The class to check the [AccessFlags] of the fields in.
|
||||||
|
* @param logger A logger.
|
||||||
|
*/
|
||||||
|
private fun ClassDef.fixFieldAccess(reference: ClassDef, logger: Logger? = null): ClassDef {
|
||||||
|
val brokenFields = fields.filterAny(reference.fields) { field, referenceField ->
|
||||||
|
if (field.name != referenceField.name) return@filterAny false
|
||||||
|
|
||||||
|
referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brokenFields.isEmpty()) return this
|
||||||
|
|
||||||
|
logger?.trace("Found ${brokenFields.size} broken fields")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a field public.
|
||||||
|
*/
|
||||||
|
fun MutableField.publicize() {
|
||||||
|
accessFlags = accessFlags.toPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
return asMutableClass().apply {
|
||||||
|
fields.filter { brokenFields.contains(it) }.forEach(MutableField::publicize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publicize methods if they are public in [reference].
|
||||||
|
*
|
||||||
|
* @param reference The class to check the [AccessFlags] of the methods in.
|
||||||
|
* @param logger A logger.
|
||||||
|
*/
|
||||||
|
private fun ClassDef.fixMethodAccess(reference: ClassDef, logger: Logger? = null): ClassDef {
|
||||||
|
val brokenMethods = methods.filterAny(reference.methods) { method, referenceMethod ->
|
||||||
|
if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false
|
||||||
|
|
||||||
|
referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brokenMethods.isEmpty()) return this
|
||||||
|
|
||||||
|
logger?.trace("Found ${brokenMethods.size} methods")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a method public.
|
||||||
|
*/
|
||||||
|
fun MutableMethod.publicize() {
|
||||||
|
accessFlags = accessFlags.toPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
return asMutableClass().apply {
|
||||||
|
methods.filter { brokenMethods.contains(it) }.forEach(MutableMethod::publicize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object Utils {
|
||||||
|
fun ClassDef.asMutableClass() = if (this is MutableClass) this else this.toMutable()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the [AccessFlags.PUBLIC] flag is set.
|
||||||
|
*
|
||||||
|
* @return True, if the flag is set.
|
||||||
|
*/
|
||||||
|
fun Int.isPublic() = AccessFlags.PUBLIC.isSet(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make [AccessFlags] public.
|
||||||
|
*
|
||||||
|
* @return The new [AccessFlags].
|
||||||
|
*/
|
||||||
|
fun Int.toPublic() = this.or(AccessFlags.PUBLIC).and(AccessFlags.PRIVATE.value.inv())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter [this] on [needles] matching the given [predicate].
|
||||||
|
*
|
||||||
|
* @param this The hay to filter for [needles].
|
||||||
|
* @param needles The needles to filter [this] with.
|
||||||
|
* @param predicate The filter.
|
||||||
|
* @return The [this] filtered on [needles] matching the given [predicate].
|
||||||
|
*/
|
||||||
|
fun <HayType, NeedleType> Iterable<HayType>.filterAny(
|
||||||
|
needles: Iterable<NeedleType>, predicate: (HayType, NeedleType) -> Boolean
|
||||||
|
) = Iterable<HayType>::filter.any(this, needles, predicate)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter [this] on [needles] not matching the given [predicate].
|
||||||
|
*
|
||||||
|
* @param this The hay to filter for [needles].
|
||||||
|
* @param needles The needles to filter [this] with.
|
||||||
|
* @param predicate The filter.
|
||||||
|
* @return The [this] filtered on [needles] not matching the given [predicate].
|
||||||
|
*/
|
||||||
|
fun <HayType, NeedleType> Iterable<HayType>.filterNotAny(
|
||||||
|
needles: Iterable<NeedleType>, predicate: (HayType, NeedleType) -> Boolean
|
||||||
|
) = Iterable<HayType>::filterNot.any(this, needles, predicate)
|
||||||
|
|
||||||
|
fun <HayType, NeedleType> KFunction2<Iterable<HayType>, (HayType) -> Boolean, List<HayType>>.any(
|
||||||
|
haystack: Iterable<HayType>,
|
||||||
|
needles: Iterable<NeedleType>,
|
||||||
|
predicate: (HayType, NeedleType) -> Boolean
|
||||||
|
) = this(haystack) { hay ->
|
||||||
|
needles.any { needle ->
|
||||||
|
predicate(hay, needle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt
Normal file
15
src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
internal class ListBackedSet<E>(private val list: MutableList<E>) : MutableSet<E> {
|
||||||
|
override val size get() = list.size
|
||||||
|
override fun add(element: E) = list.add(element)
|
||||||
|
override fun addAll(elements: Collection<E>) = list.addAll(elements)
|
||||||
|
override fun clear() = list.clear()
|
||||||
|
override fun iterator() = list.listIterator()
|
||||||
|
override fun remove(element: E) = list.remove(element)
|
||||||
|
override fun removeAll(elements: Collection<E>) = list.removeAll(elements)
|
||||||
|
override fun retainAll(elements: Collection<E>) = list.retainAll(elements)
|
||||||
|
override fun contains(element: E) = list.contains(element)
|
||||||
|
override fun containsAll(elements: Collection<E>) = list.containsAll(elements)
|
||||||
|
override fun isEmpty() = list.isEmpty()
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.ClassProxy
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that represents a set of classes and proxies.
|
||||||
|
*
|
||||||
|
* @param classes The classes to be backed by proxies.
|
||||||
|
*/
|
||||||
|
class ProxyBackedClassList(internal val classes: MutableList<ClassDef>) : Set<ClassDef> {
|
||||||
|
internal val proxies = mutableListOf<ClassProxy>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a [ClassDef].
|
||||||
|
*/
|
||||||
|
fun add(classDef: ClassDef) = classes.add(classDef)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a [ClassProxy].
|
||||||
|
*/
|
||||||
|
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace all classes with their mutated versions.
|
||||||
|
*/
|
||||||
|
internal fun replaceClasses() =
|
||||||
|
proxies.removeIf { proxy ->
|
||||||
|
// if the proxy is unused, keep it in the list
|
||||||
|
if (!proxy.resolved) return@removeIf false
|
||||||
|
|
||||||
|
// if it has been used, replace the original class with the new class
|
||||||
|
val index = classes.indexOfFirst { it.type == proxy.immutableClass.type }
|
||||||
|
classes[index] = proxy.mutableClass
|
||||||
|
|
||||||
|
// return true to remove it from the proxies list
|
||||||
|
return@removeIf true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override val size get() = classes.size
|
||||||
|
override fun contains(element: ClassDef) = classes.contains(element)
|
||||||
|
override fun containsAll(elements: Collection<ClassDef>) = classes.containsAll(elements)
|
||||||
|
override fun isEmpty() = classes.isEmpty()
|
||||||
|
override fun iterator() = classes.iterator()
|
||||||
|
}
|
||||||
19
src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt
Normal file
19
src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||||
|
|
||||||
|
object TypeUtil {
|
||||||
|
/**
|
||||||
|
* traverse the class hierarchy starting from the given root class
|
||||||
|
*
|
||||||
|
* @param targetClass the class to start traversing the class hierarchy from
|
||||||
|
* @param callback function that is called for every class in the hierarchy
|
||||||
|
*/
|
||||||
|
fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) {
|
||||||
|
callback(targetClass)
|
||||||
|
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
|
||||||
|
traverseClassHierarchy(it, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/kotlin/app/revanced/patcher/util/VersionReader.kt
Normal file
18
src/main/kotlin/app/revanced/patcher/util/VersionReader.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal object VersionReader {
|
||||||
|
@JvmStatic
|
||||||
|
private val props = Properties().apply {
|
||||||
|
load(
|
||||||
|
VersionReader::class.java.getResourceAsStream("/revanced-patcher/version.properties")
|
||||||
|
?: throw IllegalStateException("Could not load version.properties")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun read(): String {
|
||||||
|
return props.getProperty("version") ?: throw IllegalStateException("Version not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/kotlin/app/revanced/patcher/util/dex/DexFile.kt
Normal file
10
src/main/kotlin/app/revanced/patcher/util/dex/DexFile.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package app.revanced.patcher.util.dex
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for dex files.
|
||||||
|
* @param name The original name of the dex file.
|
||||||
|
* @param stream The dex file as [InputStream].
|
||||||
|
*/
|
||||||
|
data class DexFile(val name: String, val stream: InputStream)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package app.revanced.patcher.util.method
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
|
import org.jf.dexlib2.iface.reference.MethodReference
|
||||||
|
import org.jf.dexlib2.util.MethodUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a method from another method via instruction offsets.
|
||||||
|
* @param bytecodeContext The context to use when resolving the next method reference.
|
||||||
|
* @param currentMethod The method to start from.
|
||||||
|
*/
|
||||||
|
class MethodWalker internal constructor(
|
||||||
|
private val bytecodeContext: BytecodeContext,
|
||||||
|
private var currentMethod: Method
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Get the method which was walked last.
|
||||||
|
*
|
||||||
|
* It is possible to cast this method to a [MutableMethod], if the method has been walked mutably.
|
||||||
|
*
|
||||||
|
* @return The method which was walked last.
|
||||||
|
*/
|
||||||
|
fun getMethod(): Method {
|
||||||
|
return currentMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk to a method defined at the offset in the instruction list of the current method.
|
||||||
|
*
|
||||||
|
* The current method will be mutable.
|
||||||
|
*
|
||||||
|
* @param offset The offset of the instruction. This instruction must be of format 35c.
|
||||||
|
* @param walkMutable If this is true, the class of the method will be resolved mutably.
|
||||||
|
* @return The same [MethodWalker] instance with the method at [offset].
|
||||||
|
*/
|
||||||
|
fun nextMethod(offset: Int, walkMutable: Boolean = false): MethodWalker {
|
||||||
|
currentMethod.implementation?.instructions?.let { instructions ->
|
||||||
|
val instruction = instructions.elementAt(offset)
|
||||||
|
|
||||||
|
val newMethod = (instruction as ReferenceInstruction).reference as MethodReference
|
||||||
|
val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
|
||||||
|
|
||||||
|
val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
|
||||||
|
currentMethod = methods.first {
|
||||||
|
return@first MethodUtil.methodSignaturesMatch(it, newMethod)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MethodNotFoundException(exception: String) : Exception(exception)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package app.revanced.patcher.util.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.Context
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import org.jf.dexlib2.DexFileFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URLClassLoader
|
||||||
|
import java.util.jar.JarFile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A patch bundle.
|
||||||
|
|
||||||
|
* @param path The path to the patch bundle.
|
||||||
|
*/
|
||||||
|
sealed class PatchBundle(path: String) : File(path) {
|
||||||
|
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
|
||||||
|
classNames.forEach { className ->
|
||||||
|
val clazz = classLoader.loadClass(className)
|
||||||
|
if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) return@forEach
|
||||||
|
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Context>>)
|
||||||
|
}
|
||||||
|
}.sortedBy { it.patchName }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A patch bundle of type [Jar].
|
||||||
|
*
|
||||||
|
* @param patchBundlePath The path to the patch bundle.
|
||||||
|
*/
|
||||||
|
class Jar(patchBundlePath: String) : PatchBundle(patchBundlePath) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load patches from the patch bundle.
|
||||||
|
*
|
||||||
|
* Patches will be loaded with a new [URLClassLoader].
|
||||||
|
*/
|
||||||
|
fun loadPatches() = loadPatches(
|
||||||
|
URLClassLoader(
|
||||||
|
arrayOf(this.toURI().toURL()),
|
||||||
|
Thread.currentThread().contextClassLoader // TODO: find out why this is required
|
||||||
|
),
|
||||||
|
JarFile(this)
|
||||||
|
.stream()
|
||||||
|
.filter {it.name.endsWith(".class") && !it.name.contains("$")}
|
||||||
|
.map({it -> it.realName.replace('/', '.').replace(".class", "")}).iterator()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A patch bundle of type [Dex] format.
|
||||||
|
*
|
||||||
|
* @param patchBundlePath The path to a patch bundle of dex format.
|
||||||
|
* @param dexClassLoader The dex class loader.
|
||||||
|
*/
|
||||||
|
class Dex(patchBundlePath: String, private val dexClassLoader: ClassLoader) : PatchBundle(patchBundlePath) {
|
||||||
|
/**
|
||||||
|
* Load patches from the patch bundle.
|
||||||
|
*
|
||||||
|
* Patches will be loaded to the provided [dexClassLoader].
|
||||||
|
*/
|
||||||
|
fun loadPatches() = loadPatches(dexClassLoader,
|
||||||
|
DexFileFactory.loadDexFile(path, null).classes.asSequence().map({ classDef ->
|
||||||
|
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
|
||||||
|
}).iterator())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.patcher.util.proxy
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A proxy class for a [ClassDef].
|
||||||
|
*
|
||||||
|
* A class proxy simply holds a reference to the original class
|
||||||
|
* and allocates a mutable clone for the original class if needed.
|
||||||
|
* @param immutableClass The class to proxy.
|
||||||
|
*/
|
||||||
|
class ClassProxy internal constructor(
|
||||||
|
val immutableClass: ClassDef,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Weather the proxy was actually used.
|
||||||
|
*/
|
||||||
|
internal var resolved = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mutable clone of the original class.
|
||||||
|
*
|
||||||
|
* Note: This is only allocated if the proxy is actually used.
|
||||||
|
*/
|
||||||
|
val mutableClass by lazy {
|
||||||
|
resolved = true
|
||||||
|
if (immutableClass is MutableClass) {
|
||||||
|
immutableClass
|
||||||
|
} else
|
||||||
|
MutableClass(immutableClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.base.BaseAnnotation
|
||||||
|
import org.jf.dexlib2.iface.Annotation
|
||||||
|
|
||||||
|
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(): String {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getElements(): MutableSet<MutableAnnotationElement> {
|
||||||
|
return _elements
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVisibility(): Int {
|
||||||
|
return visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun Annotation.toMutable(): MutableAnnotation {
|
||||||
|
return MutableAnnotation(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.base.BaseAnnotationElement
|
||||||
|
import org.jf.dexlib2.iface.AnnotationElement
|
||||||
|
import org.jf.dexlib2.iface.value.EncodedValue
|
||||||
|
|
||||||
|
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(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(): EncodedValue {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun AnnotationElement.toMutable(): MutableAnnotationElement {
|
||||||
|
return MutableAnnotationElement(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
|
import com.google.common.collect.Iterables
|
||||||
|
import org.jf.dexlib2.base.reference.BaseTypeReference
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import org.jf.dexlib2.util.FieldUtil
|
||||||
|
import org.jf.dexlib2.util.MethodUtil
|
||||||
|
|
||||||
|
class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() {
|
||||||
|
// 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 { Iterables.filter(_methods, MethodUtil.METHOD_IS_DIRECT).toMutableSet() }
|
||||||
|
private val _virtualMethods by lazy { Iterables.filter(_methods, MethodUtil.METHOD_IS_VIRTUAL).toMutableSet() }
|
||||||
|
|
||||||
|
// Fields
|
||||||
|
private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() }
|
||||||
|
private val _staticFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_STATIC).toMutableSet() }
|
||||||
|
private val _instanceFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_INSTANCE).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(): String {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccessFlags(): Int {
|
||||||
|
return accessFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSourceFile(): String? {
|
||||||
|
return sourceFile
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSuperclass(): String? {
|
||||||
|
return superclass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInterfaces(): MutableList<String> {
|
||||||
|
return _interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAnnotations(): MutableSet<MutableAnnotation> {
|
||||||
|
return _annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStaticFields(): MutableSet<MutableField> {
|
||||||
|
return _staticFields
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInstanceFields(): MutableSet<MutableField> {
|
||||||
|
return _instanceFields
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFields(): MutableSet<MutableField> {
|
||||||
|
return _fields
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDirectMethods(): MutableSet<MutableMethod> {
|
||||||
|
return _directMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVirtualMethods(): MutableSet<MutableMethod> {
|
||||||
|
return _virtualMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMethods(): MutableSet<MutableMethod> {
|
||||||
|
return _methods
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ClassDef.toMutable(): MutableClass {
|
||||||
|
return MutableClass(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.HiddenApiRestriction
|
||||||
|
import org.jf.dexlib2.base.reference.BaseFieldReference
|
||||||
|
import org.jf.dexlib2.iface.Field
|
||||||
|
|
||||||
|
class MutableField(field: Field) : Field, BaseFieldReference() {
|
||||||
|
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(): String {
|
||||||
|
return this.definingClass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getType(): String {
|
||||||
|
return this.type
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAnnotations(): MutableSet<MutableAnnotation> {
|
||||||
|
return this._annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccessFlags(): Int {
|
||||||
|
return this.accessFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHiddenApiRestrictions(): MutableSet<HiddenApiRestriction> {
|
||||||
|
return this._hiddenApiRestrictions
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInitialValue(): MutableEncodedValue? {
|
||||||
|
return this.initialValue
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun Field.toMutable(): MutableField {
|
||||||
|
return MutableField(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethodParameter.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.HiddenApiRestriction
|
||||||
|
import org.jf.dexlib2.base.reference.BaseMethodReference
|
||||||
|
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
|
||||||
|
class MutableMethod(method: Method) : Method, BaseMethodReference() {
|
||||||
|
private var definingClass = method.definingClass
|
||||||
|
private var name = method.name
|
||||||
|
private var accessFlags = method.accessFlags
|
||||||
|
private var returnType = method.returnType
|
||||||
|
|
||||||
|
// Create own mutable MethodImplementation (due to not being able to change members like register count)
|
||||||
|
private val _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } }
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDefiningClass(): String {
|
||||||
|
return definingClass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getParameterTypes(): MutableList<CharSequence> {
|
||||||
|
return _parameterTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReturnType(): String {
|
||||||
|
return returnType
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAnnotations(): MutableSet<MutableAnnotation> {
|
||||||
|
return _annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccessFlags(): Int {
|
||||||
|
return accessFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHiddenApiRestrictions(): MutableSet<HiddenApiRestriction> {
|
||||||
|
return _hiddenApiRestrictions
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getParameters(): MutableList<MutableMethodParameter> {
|
||||||
|
return _parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getImplementation(): MutableMethodImplementation? {
|
||||||
|
return _implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun Method.toMutable(): MutableMethod {
|
||||||
|
return MutableMethod(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.base.BaseMethodParameter
|
||||||
|
import org.jf.dexlib2.iface.MethodParameter
|
||||||
|
|
||||||
|
// TODO: finish overriding all members if necessary
|
||||||
|
class MutableMethodParameter(parameter: MethodParameter) : MethodParameter, BaseMethodParameter() {
|
||||||
|
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(): String {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String? {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSignature(): String? {
|
||||||
|
return signature
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAnnotations(): MutableSet<MutableAnnotation> {
|
||||||
|
return _annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun MethodParameter.toMutable(): MutableMethodParameter {
|
||||||
|
return MutableMethodParameter(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.base.value.BaseAnnotationEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.AnnotationElement
|
||||||
|
import org.jf.dexlib2.iface.value.AnnotationEncodedValue
|
||||||
|
|
||||||
|
class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) : BaseAnnotationEncodedValue(),
|
||||||
|
MutableEncodedValue {
|
||||||
|
private var type = annotationEncodedValue.type
|
||||||
|
|
||||||
|
private val _elements by lazy {
|
||||||
|
annotationEncodedValue.elements.map { annotationElement -> annotationElement.toMutable() }.toMutableSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getType(): String {
|
||||||
|
return this.type
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setType(type: String) {
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getElements(): MutableSet<out AnnotationElement> {
|
||||||
|
return _elements
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun AnnotationEncodedValue.toMutable(): MutableAnnotationEncodedValue {
|
||||||
|
return MutableAnnotationEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.base.value.BaseArrayEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.ArrayEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.EncodedValue
|
||||||
|
|
||||||
|
class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) : BaseArrayEncodedValue(), MutableEncodedValue {
|
||||||
|
private val _value by lazy {
|
||||||
|
arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(): MutableList<out EncodedValue> {
|
||||||
|
return _value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue {
|
||||||
|
return MutableArrayEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseBooleanEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.BooleanEncodedValue
|
||||||
|
|
||||||
|
class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) : BaseBooleanEncodedValue(),
|
||||||
|
MutableEncodedValue {
|
||||||
|
private var value = booleanEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValue(): Boolean {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: Boolean) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun BooleanEncodedValue.toMutable(): MutableBooleanEncodedValue {
|
||||||
|
return MutableBooleanEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseByteEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.ByteEncodedValue
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue {
|
||||||
|
return MutableByteEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseCharEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.CharEncodedValue
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun CharEncodedValue.toMutable(): MutableCharEncodedValue {
|
||||||
|
return MutableCharEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseDoubleEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.DoubleEncodedValue
|
||||||
|
|
||||||
|
class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) : BaseDoubleEncodedValue(),
|
||||||
|
MutableEncodedValue {
|
||||||
|
private var value = doubleEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValue(): Double {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: Double) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun DoubleEncodedValue.toMutable(): MutableDoubleEncodedValue {
|
||||||
|
return MutableDoubleEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.ValueType
|
||||||
|
import org.jf.dexlib2.iface.value.*
|
||||||
|
|
||||||
|
interface MutableEncodedValue : EncodedValue {
|
||||||
|
companion object {
|
||||||
|
fun EncodedValue.toMutable(): MutableEncodedValue {
|
||||||
|
return when (this.valueType) {
|
||||||
|
ValueType.TYPE -> MutableTypeEncodedValue(this as TypeEncodedValue)
|
||||||
|
ValueType.FIELD -> MutableFieldEncodedValue(this as FieldEncodedValue)
|
||||||
|
ValueType.METHOD -> MutableMethodEncodedValue(this as MethodEncodedValue)
|
||||||
|
ValueType.ENUM -> MutableEnumEncodedValue(this as EnumEncodedValue)
|
||||||
|
ValueType.ARRAY -> MutableArrayEncodedValue(this as ArrayEncodedValue)
|
||||||
|
ValueType.ANNOTATION -> MutableAnnotationEncodedValue(this as AnnotationEncodedValue)
|
||||||
|
ValueType.BYTE -> MutableByteEncodedValue(this as ByteEncodedValue)
|
||||||
|
ValueType.SHORT -> MutableShortEncodedValue(this as ShortEncodedValue)
|
||||||
|
ValueType.CHAR -> MutableCharEncodedValue(this as CharEncodedValue)
|
||||||
|
ValueType.INT -> MutableIntEncodedValue(this as IntEncodedValue)
|
||||||
|
ValueType.LONG -> MutableLongEncodedValue(this as LongEncodedValue)
|
||||||
|
ValueType.FLOAT -> MutableFloatEncodedValue(this as FloatEncodedValue)
|
||||||
|
ValueType.DOUBLE -> MutableDoubleEncodedValue(this as DoubleEncodedValue)
|
||||||
|
ValueType.METHOD_TYPE -> MutableMethodTypeEncodedValue(this as MethodTypeEncodedValue)
|
||||||
|
ValueType.METHOD_HANDLE -> MutableMethodHandleEncodedValue(this as MethodHandleEncodedValue)
|
||||||
|
ValueType.STRING -> MutableStringEncodedValue(this as StringEncodedValue)
|
||||||
|
ValueType.BOOLEAN -> MutableBooleanEncodedValue(this as BooleanEncodedValue)
|
||||||
|
ValueType.NULL -> MutableNullEncodedValue()
|
||||||
|
else -> this as MutableEncodedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseEnumEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.reference.FieldReference
|
||||||
|
import org.jf.dexlib2.iface.value.EnumEncodedValue
|
||||||
|
|
||||||
|
class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) : BaseEnumEncodedValue(), MutableEncodedValue {
|
||||||
|
private var value = enumEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValue(): FieldReference {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: FieldReference) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun EnumEncodedValue.toMutable(): MutableEnumEncodedValue {
|
||||||
|
return MutableEnumEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.ValueType
|
||||||
|
import org.jf.dexlib2.base.value.BaseFieldEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.reference.FieldReference
|
||||||
|
import org.jf.dexlib2.iface.value.FieldEncodedValue
|
||||||
|
|
||||||
|
class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) : BaseFieldEncodedValue(), MutableEncodedValue {
|
||||||
|
private var value = fieldEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValueType(): Int {
|
||||||
|
return ValueType.FIELD
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(): FieldReference {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: FieldReference) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun FieldEncodedValue.toMutable(): MutableFieldEncodedValue {
|
||||||
|
return MutableFieldEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseFloatEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.FloatEncodedValue
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue {
|
||||||
|
return MutableFloatEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseIntEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.IntEncodedValue
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun IntEncodedValue.toMutable(): MutableIntEncodedValue {
|
||||||
|
return MutableIntEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseLongEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.LongEncodedValue
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun LongEncodedValue.toMutable(): MutableLongEncodedValue {
|
||||||
|
return MutableLongEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseMethodEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.reference.MethodReference
|
||||||
|
import org.jf.dexlib2.iface.value.MethodEncodedValue
|
||||||
|
|
||||||
|
class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) : BaseMethodEncodedValue(),
|
||||||
|
MutableEncodedValue {
|
||||||
|
private var value = methodEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValue(): MethodReference {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: MethodReference) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun MethodEncodedValue.toMutable(): MutableMethodEncodedValue {
|
||||||
|
return MutableMethodEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseMethodHandleEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.reference.MethodHandleReference
|
||||||
|
import org.jf.dexlib2.iface.value.MethodHandleEncodedValue
|
||||||
|
|
||||||
|
class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEncodedValue) :
|
||||||
|
BaseMethodHandleEncodedValue(),
|
||||||
|
MutableEncodedValue {
|
||||||
|
private var value = methodHandleEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValue(): MethodHandleReference {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: MethodHandleReference) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun MethodHandleEncodedValue.toMutable(): MutableMethodHandleEncodedValue {
|
||||||
|
return MutableMethodHandleEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseMethodTypeEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.reference.MethodProtoReference
|
||||||
|
import org.jf.dexlib2.iface.value.MethodTypeEncodedValue
|
||||||
|
|
||||||
|
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) : BaseMethodTypeEncodedValue(),
|
||||||
|
MutableEncodedValue {
|
||||||
|
private var value = methodTypeEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValue(): MethodProtoReference {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: MethodProtoReference) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun MethodTypeEncodedValue.toMutable(): MutableMethodTypeEncodedValue {
|
||||||
|
return MutableMethodTypeEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseNullEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.ByteEncodedValue
|
||||||
|
|
||||||
|
class MutableNullEncodedValue : BaseNullEncodedValue(), MutableEncodedValue {
|
||||||
|
companion object {
|
||||||
|
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue {
|
||||||
|
return MutableByteEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseShortEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.ShortEncodedValue
|
||||||
|
|
||||||
|
class MutableShortEncodedValue(shortEncodedValue: ShortEncodedValue) : BaseShortEncodedValue(), MutableEncodedValue {
|
||||||
|
private var value = shortEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValue(): Short {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: Short) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ShortEncodedValue.toMutable(): MutableShortEncodedValue {
|
||||||
|
return MutableShortEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseStringEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.ByteEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.StringEncodedValue
|
||||||
|
|
||||||
|
class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) : BaseStringEncodedValue(),
|
||||||
|
MutableEncodedValue {
|
||||||
|
private var value = stringEncodedValue.value
|
||||||
|
|
||||||
|
override fun getValue(): String {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(value: String) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue {
|
||||||
|
return MutableByteEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||||
|
|
||||||
|
import org.jf.dexlib2.base.value.BaseTypeEncodedValue
|
||||||
|
import org.jf.dexlib2.iface.value.TypeEncodedValue
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue {
|
||||||
|
return MutableTypeEncodedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package app.revanced.patcher.util.smali
|
||||||
|
|
||||||
|
import org.jf.dexlib2.iface.instruction.Instruction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,84 @@
|
|||||||
|
package app.revanced.patcher.util.smali
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
|
import org.antlr.runtime.CommonTokenStream
|
||||||
|
import org.antlr.runtime.TokenSource
|
||||||
|
import org.antlr.runtime.tree.CommonTreeNodeStream
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
import org.jf.dexlib2.Opcodes
|
||||||
|
import org.jf.dexlib2.builder.BuilderInstruction
|
||||||
|
import org.jf.dexlib2.writer.builder.DexBuilder
|
||||||
|
import org.jf.smali.LexerErrorInterface
|
||||||
|
import org.jf.smali.smaliFlexLexer
|
||||||
|
import org.jf.smali.smaliParser
|
||||||
|
import org.jf.smali.smaliTreeWalker
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
private const val METHOD_TEMPLATE = """
|
||||||
|
.class LInlineCompiler;
|
||||||
|
.super Ljava/lang/Object;
|
||||||
|
.method %s dummyMethod(%s)V
|
||||||
|
.registers %d
|
||||||
|
%s
|
||||||
|
.end method
|
||||||
|
"""
|
||||||
|
|
||||||
|
class InlineSmaliCompiler {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Compiles a string of Smali code to a list of instructions.
|
||||||
|
* Special registers (such as p0, p1) will only work correctly
|
||||||
|
* if the parameters and registers of the method are passed.
|
||||||
|
*/
|
||||||
|
fun compile(
|
||||||
|
instructions: String, parameters: String, registers: Int, forStaticMethod: Boolean
|
||||||
|
): List<BuilderInstruction> {
|
||||||
|
val input = METHOD_TEMPLATE.format(
|
||||||
|
if (forStaticMethod) {
|
||||||
|
"static"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}, parameters, registers, instructions
|
||||||
|
)
|
||||||
|
val reader = InputStreamReader(input.byteInputStream())
|
||||||
|
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
|
||||||
|
val tokens = CommonTokenStream(lexer as TokenSource)
|
||||||
|
val parser = smaliParser(tokens)
|
||||||
|
val result = parser.smali_file()
|
||||||
|
if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val treeStream = CommonTreeNodeStream(result.tree)
|
||||||
|
treeStream.tokenStream = tokens
|
||||||
|
val dexGen = smaliTreeWalker(treeStream)
|
||||||
|
dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault()))
|
||||||
|
val classDef = dexGen.smali_file()
|
||||||
|
return classDef.methods.first().implementation!!.instructions.map { it as BuilderInstruction }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 method The method to compile the instructions against.
|
||||||
|
* @returns A list of instructions.
|
||||||
|
*/
|
||||||
|
fun String.toInstructions(method: MutableMethod? = null): List<BuilderInstruction> {
|
||||||
|
return InlineSmaliCompiler.compile(this,
|
||||||
|
method?.parameters?.joinToString("") { it } ?: "",
|
||||||
|
method?.implementation?.registerCount ?: 1,
|
||||||
|
method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile a line of Smali code to an instruction.
|
||||||
|
* @param templateMethod The method to compile the instructions against.
|
||||||
|
* @return The instruction.
|
||||||
|
*/
|
||||||
|
fun String.toInstruction(templateMethod: MutableMethod? = null) = this.toInstructions(templateMethod).first()
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package net.revanced.patcher
|
|
||||||
|
|
||||||
import net.revanced.patcher.cache.Cache
|
|
||||||
import net.revanced.patcher.patch.Patch
|
|
||||||
import net.revanced.patcher.resolver.MethodResolver
|
|
||||||
import net.revanced.patcher.signature.Signature
|
|
||||||
import net.revanced.patcher.util.Io
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The patcher. (docs WIP)
|
|
||||||
*
|
|
||||||
* @param input the input stream to read from, must be a JAR
|
|
||||||
* @param signatures the signatures
|
|
||||||
* @sample net.revanced.patcher.PatcherTest
|
|
||||||
*/
|
|
||||||
class Patcher(
|
|
||||||
private val input: InputStream,
|
|
||||||
signatures: Array<Signature>,
|
|
||||||
) {
|
|
||||||
var cache: Cache
|
|
||||||
private val patches: MutableList<Patch> = mutableListOf()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val classes = Io.readClassesFromJar(input);
|
|
||||||
cache = Cache(classes, MethodResolver(classes, signatures).resolve())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addPatches(vararg patches: Patch) {
|
|
||||||
this.patches.addAll(patches)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun applyPatches(stopOnError: Boolean = false): Map<String, Result<Nothing?>> {
|
|
||||||
return buildMap {
|
|
||||||
for (patch in patches) {
|
|
||||||
val result: Result<Nothing?> = try {
|
|
||||||
val pr = patch.execute()
|
|
||||||
if (pr.isSuccess()) continue
|
|
||||||
Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.failure(e)
|
|
||||||
}
|
|
||||||
this[patch.patchName] = result
|
|
||||||
if (stopOnError && result.isFailure) break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveTo(output: OutputStream) {
|
|
||||||
Io.writeClassesToJar(input, output, cache.classes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package net.revanced.patcher.cache
|
|
||||||
|
|
||||||
import org.objectweb.asm.tree.ClassNode
|
|
||||||
|
|
||||||
class Cache (
|
|
||||||
val classes: List<ClassNode>,
|
|
||||||
val methods: MethodMap
|
|
||||||
)
|
|
||||||
|
|
||||||
class MethodMap : LinkedHashMap<String, PatchData>() {
|
|
||||||
override fun get(key: String): PatchData {
|
|
||||||
return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MethodNotFoundException(s: String) : Exception(s)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package net.revanced.patcher.cache
|
|
||||||
|
|
||||||
import org.objectweb.asm.tree.ClassNode
|
|
||||||
import org.objectweb.asm.tree.MethodNode
|
|
||||||
|
|
||||||
data class PatchData(
|
|
||||||
val declaringClass: ClassNode,
|
|
||||||
val method: MethodNode,
|
|
||||||
val scanData: PatternScanData
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PatternScanData(
|
|
||||||
val startIndex: Int,
|
|
||||||
val endIndex: Int
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package net.revanced.patcher.patch
|
|
||||||
|
|
||||||
class Patch(val patchName: String, val fn: () -> PatchResult) {
|
|
||||||
fun execute(): PatchResult {
|
|
||||||
return fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package net.revanced.patcher.resolver
|
|
||||||
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import net.revanced.patcher.cache.MethodMap
|
|
||||||
import net.revanced.patcher.cache.PatchData
|
|
||||||
import net.revanced.patcher.cache.PatternScanData
|
|
||||||
import net.revanced.patcher.signature.Signature
|
|
||||||
import net.revanced.patcher.util.ExtraTypes
|
|
||||||
import org.objectweb.asm.Type
|
|
||||||
import org.objectweb.asm.tree.ClassNode
|
|
||||||
import org.objectweb.asm.tree.InsnList
|
|
||||||
import org.objectweb.asm.tree.MethodNode
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger("MethodResolver")
|
|
||||||
|
|
||||||
internal class MethodResolver(private val classList: List<ClassNode>, private val signatures: Array<Signature>) {
|
|
||||||
fun resolve(): MethodMap {
|
|
||||||
val methodMap = MethodMap()
|
|
||||||
|
|
||||||
for ((classNode, methods) in classList) {
|
|
||||||
for (method in methods) {
|
|
||||||
for (signature in signatures) {
|
|
||||||
if (methodMap.containsKey(signature.name)) { // method already found for this sig
|
|
||||||
logger.debug { "Sig ${signature.name} already found, skipping." }
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.debug { "Resolving sig ${signature.name}: ${classNode.name} / ${method.name}" }
|
|
||||||
val (r, sr) = this.cmp(method, signature)
|
|
||||||
if (!r || sr == null) {
|
|
||||||
logger.debug { "Compare result for sig ${signature.name} has failed!" }
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.debug { "Method for sig ${signature.name} found!" }
|
|
||||||
methodMap[signature.name] = PatchData(
|
|
||||||
classNode,
|
|
||||||
method,
|
|
||||||
PatternScanData(
|
|
||||||
// sadly we cannot create contracts for a data class, so we must assert
|
|
||||||
sr.startIndex!!,
|
|
||||||
sr.endIndex!!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (signature in signatures) {
|
|
||||||
if (methodMap.containsKey(signature.name)) continue
|
|
||||||
logger.error { "Could not find method for sig ${signature.name}!" }
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodMap
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cmp(method: MethodNode, signature: Signature): Pair<Boolean, ScanResult?> {
|
|
||||||
val returns = Type.getReturnType(method.desc).convertObject()
|
|
||||||
if (signature.returns != returns) {
|
|
||||||
logger.debug {
|
|
||||||
"""
|
|
||||||
Comparing sig ${signature.name}: invalid return type:
|
|
||||||
expected ${signature.returns}},
|
|
||||||
got $returns
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
return false to null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signature.accessors != method.access) {
|
|
||||||
logger.debug { "Comparing sig ${signature.name}: invalid accessors:\nexpected ${signature.accessors},\ngot ${method.access}" }
|
|
||||||
return false to null
|
|
||||||
}
|
|
||||||
|
|
||||||
val parameters = Type.getArgumentTypes(method.desc).convertObjects()
|
|
||||||
if (!signature.parameters.contentEquals(parameters)) {
|
|
||||||
logger.debug {
|
|
||||||
"""
|
|
||||||
Comparing sig ${signature.name}: invalid parameter types:
|
|
||||||
expected ${signature.parameters.joinToString()}},
|
|
||||||
got ${parameters.joinToString()}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
return false to null
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = method.instructions.scanFor(signature.opcodes)
|
|
||||||
if (!result.found) {
|
|
||||||
logger.debug { "Comparing sig ${signature.name}: invalid opcode pattern" }
|
|
||||||
return false to null
|
|
||||||
}
|
|
||||||
|
|
||||||
return true to result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private operator fun ClassNode.component1(): ClassNode {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private operator fun ClassNode.component2(): List<MethodNode> {
|
|
||||||
return this.methods
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun InsnList.scanFor(pattern: Array<Int>): ScanResult {
|
|
||||||
for (i in 0 until this.size()) {
|
|
||||||
var occurrence = 0
|
|
||||||
while (i + occurrence < this.size()) {
|
|
||||||
if (this[i + occurrence].opcode != pattern[occurrence]) break
|
|
||||||
if (++occurrence >= pattern.size) {
|
|
||||||
val current = i + occurrence
|
|
||||||
return ScanResult(true, current - pattern.size, current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ScanResult(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Type.convertObject(): Type {
|
|
||||||
return when (this.sort) {
|
|
||||||
Type.OBJECT -> ExtraTypes.Any
|
|
||||||
Type.ARRAY -> ExtraTypes.ArrayAny
|
|
||||||
else -> this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Array<Type>.convertObjects(): Array<Type> {
|
|
||||||
return this.map { it.convertObject() }.toTypedArray()
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package net.revanced.patcher.resolver
|
|
||||||
|
|
||||||
internal data class ScanResult(
|
|
||||||
val found: Boolean,
|
|
||||||
val startIndex: Int? = 0,
|
|
||||||
val endIndex: Int? = 0
|
|
||||||
)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package net.revanced.patcher.signature
|
|
||||||
|
|
||||||
import org.objectweb.asm.Type
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ASM signature list for the Patcher.
|
|
||||||
*
|
|
||||||
* @param name The name of the method.
|
|
||||||
* Do not use the actual method name, instead try to guess what the method name originally was.
|
|
||||||
* If you are unable to guess a method name, doing something like "patch-name-1" is fine too.
|
|
||||||
* For example: "override-codec-1".
|
|
||||||
* This method name will be used to find the corresponding patch.
|
|
||||||
* @param returns The return type/signature of the method.
|
|
||||||
* @param accessors The accessors of the method.
|
|
||||||
* @param parameters The parameter types of the method.
|
|
||||||
* @param opcodes The opcode pattern of the method, used to find the method by pattern scanning.
|
|
||||||
*/
|
|
||||||
@Suppress("ArrayInDataClass")
|
|
||||||
data class Signature(
|
|
||||||
val name: String,
|
|
||||||
val returns: Type,
|
|
||||||
val accessors: Int,
|
|
||||||
val parameters: Array<Type>,
|
|
||||||
val opcodes: Array<Int>
|
|
||||||
)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package net.revanced.patcher.util
|
|
||||||
|
|
||||||
import org.objectweb.asm.Type
|
|
||||||
|
|
||||||
object ExtraTypes {
|
|
||||||
/**
|
|
||||||
* Any object type.
|
|
||||||
* Should be used instead of types such as: "Ljava/lang/String;"
|
|
||||||
*/
|
|
||||||
val Any: Type = Type.getType(Object::class.java)
|
|
||||||
val ArrayAny: Type = Type.getType(Array<Any>::class.java)
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package net.revanced.patcher.util
|
|
||||||
|
|
||||||
import org.objectweb.asm.ClassReader
|
|
||||||
import org.objectweb.asm.ClassWriter
|
|
||||||
import org.objectweb.asm.tree.ClassNode
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.util.jar.JarEntry
|
|
||||||
import java.util.jar.JarInputStream
|
|
||||||
import java.util.jar.JarOutputStream
|
|
||||||
|
|
||||||
object Io {
|
|
||||||
fun readClassesFromJar(input: InputStream) = mutableListOf<ClassNode>().apply {
|
|
||||||
val jar = JarInputStream(input)
|
|
||||||
while (true) {
|
|
||||||
val e = jar.nextJarEntry ?: break
|
|
||||||
if (e.name.endsWith(".class")) {
|
|
||||||
val classNode = ClassNode()
|
|
||||||
ClassReader(jar.readAllBytes()).accept(classNode, ClassReader.EXPAND_FRAMES)
|
|
||||||
this.add(classNode)
|
|
||||||
}
|
|
||||||
jar.closeEntry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun writeClassesToJar(input: InputStream, output: OutputStream, classes: List<ClassNode>) {
|
|
||||||
val jis = JarInputStream(input)
|
|
||||||
val jos = JarOutputStream(output)
|
|
||||||
|
|
||||||
// TODO: Add support for adding new/custom classes
|
|
||||||
while (true) {
|
|
||||||
val next = jis.nextJarEntry ?: break
|
|
||||||
val e = JarEntry(next) // clone it, to not modify the input (if possible)
|
|
||||||
jos.putNextEntry(e)
|
|
||||||
|
|
||||||
val clazz = classes.singleOrNull {
|
|
||||||
clazz -> clazz.name == e.name
|
|
||||||
};
|
|
||||||
if (clazz != null) {
|
|
||||||
val cw = ClassWriter(ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES)
|
|
||||||
clazz.accept(cw)
|
|
||||||
jos.write(cw.toByteArray())
|
|
||||||
} else {
|
|
||||||
jos.write(jis.readAllBytes())
|
|
||||||
}
|
|
||||||
jos.closeEntry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package net.revanced.patcher.writer
|
|
||||||
|
|
||||||
import org.objectweb.asm.tree.AbstractInsnNode
|
|
||||||
import org.objectweb.asm.tree.InsnList
|
|
||||||
|
|
||||||
object ASMWriter {
|
|
||||||
fun InsnList.setAt(index: Int, node: AbstractInsnNode) {
|
|
||||||
this[this.get(index)] = node
|
|
||||||
}
|
|
||||||
fun InsnList.insertAt(index: Int, vararg nodes: AbstractInsnNode) {
|
|
||||||
this.insert(this.get(index), nodes.toInsnList())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(Sculas): Should this be public?
|
|
||||||
private fun Array<out AbstractInsnNode>.toInsnList(): InsnList {
|
|
||||||
val list = InsnList()
|
|
||||||
this.forEach { list.add(it) }
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
src/main/resources/revanced-patcher/version.properties
Normal file
1
src/main/resources/revanced-patcher/version.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
version=${projectVersion}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.smali.ExternalLabel
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
import org.jf.dexlib2.Opcode
|
||||||
|
import org.jf.dexlib2.builder.BuilderOffsetInstruction
|
||||||
|
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.builder.instruction.BuilderInstruction21s
|
||||||
|
import org.jf.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableMethod
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
private object InstructionExtensionsTest {
|
||||||
|
private lateinit var testMethod: MutableMethod
|
||||||
|
private lateinit var testMethodImplementation: MutableMethodImplementation
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun createTestMethod() = ImmutableMethod(
|
||||||
|
"TestClass;",
|
||||||
|
"testMethod",
|
||||||
|
null,
|
||||||
|
"V",
|
||||||
|
AccessFlags.PUBLIC.value,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
|
||||||
|
repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
|
||||||
|
},
|
||||||
|
).let { testMethod = it.toMutable() }
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
testMethodImplementation.apply(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyToMethod(block: MutableMethod.() -> Unit) {
|
||||||
|
testMethod.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)
|
||||||
|
}
|
||||||
18
src/test/kotlin/app/revanced/patcher/issues/Issue98.kt
Normal file
18
src/test/kotlin/app/revanced/patcher/issues/Issue98.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.patcher.issues
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.PatchOption
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
internal class Issue98 {
|
||||||
|
companion object {
|
||||||
|
var key1: String? by PatchOption.StringOption(
|
||||||
|
"key1", null, "title", "description"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should infer nullable type correctly`() {
|
||||||
|
assertNull(key1)
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt
Normal file
113
src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
|
internal class PatchOptionsTest {
|
||||||
|
private val options = ExampleBytecodePatch.options
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should not throw an exception`() {
|
||||||
|
for (option in options) {
|
||||||
|
when (option) {
|
||||||
|
is PatchOption.StringOption -> {
|
||||||
|
option.value = "Hello World"
|
||||||
|
}
|
||||||
|
|
||||||
|
is PatchOption.BooleanOption -> {
|
||||||
|
option.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
is PatchOption.StringListOption -> {
|
||||||
|
option.value = option.options.first()
|
||||||
|
for (choice in option.options) {
|
||||||
|
println(choice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PatchOption.IntListOption -> {
|
||||||
|
option.value = option.options.first()
|
||||||
|
for (choice in option.options) {
|
||||||
|
println(choice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PatchOption.PathOption -> {
|
||||||
|
option.value = Path("test.txt").pathString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val option = options.get<String>("key1")
|
||||||
|
// or: val option: String? by options["key1"]
|
||||||
|
// then you won't need `.value` every time
|
||||||
|
println(option.value)
|
||||||
|
options["key1"] = "Hello, world!"
|
||||||
|
println(option.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return a different value when changed`() {
|
||||||
|
var value: String? by options["key1"]
|
||||||
|
val current = value + "" // force a copy
|
||||||
|
value = "Hello, world!"
|
||||||
|
assertNotEquals(current, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should be able to set value to null`() {
|
||||||
|
// Sadly, doing:
|
||||||
|
// > options["key2"] = null
|
||||||
|
// is not possible because Kotlin
|
||||||
|
// cannot reify the type "Nothing?".
|
||||||
|
// So we have to do this instead:
|
||||||
|
options["key2"] = null as Any?
|
||||||
|
// This is a cleaner replacement for the above:
|
||||||
|
options.nullify("key2")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should fail because the option does not exist`() {
|
||||||
|
assertThrows<NoSuchOptionException> {
|
||||||
|
options["this option does not exist"] = 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should fail because of invalid value type when setting an option`() {
|
||||||
|
assertThrows<InvalidTypeException> {
|
||||||
|
options["key1"] = 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should fail because of invalid value type when getting an option`() {
|
||||||
|
assertThrows<InvalidTypeException> {
|
||||||
|
options.get<Int>("key1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should fail because of an illegal value`() {
|
||||||
|
assertThrows<IllegalValueException> {
|
||||||
|
options["key3"] = "this value is not an allowed option"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should fail because the requirement is not met`() {
|
||||||
|
assertThrows<RequirementNotMetException> {
|
||||||
|
options.nullify("key1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should fail because getting a non-initialized option is illegal`() {
|
||||||
|
assertThrows<RequirementNotMetException> {
|
||||||
|
println(options["key5"].value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.patcher.usage.bytecode
|
||||||
|
|
||||||
|
import app.revanced.patcher.annotation.Compatibility
|
||||||
|
import app.revanced.patcher.annotation.Package
|
||||||
|
|
||||||
|
@Compatibility(
|
||||||
|
[Package(
|
||||||
|
"com.example.examplePackage", arrayOf("0.0.1", "0.0.2")
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
internal annotation class ExampleBytecodeCompatibility
|
||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package app.revanced.patcher.usage.bytecode
|
||||||
|
|
||||||
|
import app.revanced.patcher.annotation.Description
|
||||||
|
import app.revanced.patcher.annotation.Name
|
||||||
|
import app.revanced.patcher.annotation.Version
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||||
|
import app.revanced.patcher.extensions.or
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
|
import app.revanced.patcher.patch.*
|
||||||
|
import app.revanced.patcher.patch.annotations.DependsOn
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
|
||||||
|
import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
|
import com.google.common.collect.ImmutableList
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
import org.jf.dexlib2.Format
|
||||||
|
import org.jf.dexlib2.Opcode
|
||||||
|
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.builder.instruction.BuilderInstruction11x
|
||||||
|
import org.jf.dexlib2.builder.instruction.BuilderInstruction21c
|
||||||
|
import org.jf.dexlib2.iface.instruction.formats.Instruction21c
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableField
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableMethod
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.immutable.reference.ImmutableFieldReference
|
||||||
|
import org.jf.dexlib2.immutable.reference.ImmutableStringReference
|
||||||
|
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
|
||||||
|
import org.jf.dexlib2.util.Preconditions
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
|
||||||
|
@Patch
|
||||||
|
@Name("example-bytecode-patch")
|
||||||
|
@Description("Example demonstration of a bytecode patch.")
|
||||||
|
@ExampleResourceCompatibility
|
||||||
|
@Version("0.0.1")
|
||||||
|
@DependsOn([ExampleResourcePatch::class])
|
||||||
|
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
||||||
|
// This function will be executed by the patcher.
|
||||||
|
// You can treat it as a constructor
|
||||||
|
override fun execute(context: BytecodeContext): PatchResult {
|
||||||
|
// Get the resolved method by its fingerprint from the resolver cache
|
||||||
|
val result = ExampleFingerprint.result!!
|
||||||
|
|
||||||
|
// Patch options
|
||||||
|
println(key1)
|
||||||
|
key2 = false
|
||||||
|
|
||||||
|
// Get the implementation for the resolved method
|
||||||
|
val method = result.mutableMethod
|
||||||
|
val implementation = method.implementation!!
|
||||||
|
|
||||||
|
// Let's modify it, so it prints "Hello, ReVanced! Editing bytecode."
|
||||||
|
// Get the start index of our opcode pattern.
|
||||||
|
// This will be the index of the instruction with the opcode CONST_STRING.
|
||||||
|
val startIndex = result.scanResult.patternScanResult!!.startIndex
|
||||||
|
|
||||||
|
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
|
||||||
|
|
||||||
|
// Get the class in which the method matching our fingerprint is defined in.
|
||||||
|
val mainClass = context.findClass {
|
||||||
|
it.type == result.classDef.type
|
||||||
|
}!!.mutableClass
|
||||||
|
|
||||||
|
// Add a new method returning a string
|
||||||
|
mainClass.methods.add(
|
||||||
|
ImmutableMethod(
|
||||||
|
result.classDef.type,
|
||||||
|
"returnHello",
|
||||||
|
null,
|
||||||
|
"Ljava/lang/String;",
|
||||||
|
AccessFlags.PRIVATE or AccessFlags.STATIC,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ImmutableMethodImplementation(
|
||||||
|
1,
|
||||||
|
ImmutableList.of(
|
||||||
|
BuilderInstruction21c(
|
||||||
|
Opcode.CONST_STRING,
|
||||||
|
0,
|
||||||
|
ImmutableStringReference("Hello, ReVanced! Adding bytecode.")
|
||||||
|
),
|
||||||
|
BuilderInstruction11x(Opcode.RETURN_OBJECT, 0)
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
).toMutable()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add a field in the main class
|
||||||
|
// We will use this field in our method below to call println on
|
||||||
|
// The field holds the Ljava/io/PrintStream->out; field
|
||||||
|
mainClass.fields.add(
|
||||||
|
ImmutableField(
|
||||||
|
mainClass.type,
|
||||||
|
"dummyField",
|
||||||
|
"Ljava/io/PrintStream;",
|
||||||
|
AccessFlags.PRIVATE or AccessFlags.STATIC,
|
||||||
|
ImmutableFieldEncodedValue(
|
||||||
|
ImmutableFieldReference(
|
||||||
|
"Ljava/lang/System;",
|
||||||
|
"out",
|
||||||
|
"Ljava/io/PrintStream;"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
).toMutable()
|
||||||
|
)
|
||||||
|
|
||||||
|
// store the fields initial value into the first virtual register
|
||||||
|
method.replaceInstruction(0, "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;")
|
||||||
|
|
||||||
|
// Now let's create a new call to our method and print the return value!
|
||||||
|
// You can also use the smali compiler to create instructions.
|
||||||
|
// For this sake of example I reuse the TestClass field dummyField inside the virtual register 0.
|
||||||
|
//
|
||||||
|
// Control flow instructions are not supported as of now.
|
||||||
|
method.addInstructionsWithLabels(
|
||||||
|
startIndex + 2,
|
||||||
|
"""
|
||||||
|
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
|
||||||
|
move-result-object v1
|
||||||
|
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
// Finally, tell the patcher that this patch was a success.
|
||||||
|
// You can also return PatchResultError with a message.
|
||||||
|
// If an exception is thrown inside this function,
|
||||||
|
// a PatchResultError will be returned with the error message.
|
||||||
|
return PatchResultSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the string for an instruction at the given index with a new one.
|
||||||
|
* @param index The index of the instruction to replace the string for
|
||||||
|
* @param string The replacing string
|
||||||
|
*/
|
||||||
|
private fun MutableMethodImplementation.replaceStringAt(index: Int, string: String) {
|
||||||
|
val instruction = this.instructions[index]
|
||||||
|
|
||||||
|
// Utility method of dexlib2
|
||||||
|
Preconditions.checkFormat(instruction.opcode, Format.Format21c)
|
||||||
|
|
||||||
|
// Cast this to an instruction of the format 21c
|
||||||
|
// The instruction format can be found in the docs at
|
||||||
|
// https://source.android.com/devices/tech/dalvik/dalvik-bytecode
|
||||||
|
val strInstruction = instruction as Instruction21c
|
||||||
|
|
||||||
|
// In our case we want an instruction with the opcode CONST_STRING
|
||||||
|
// The format is 21c, so we create a new BuilderInstruction21c
|
||||||
|
// This instruction will hold the string reference constant in the virtual register of the original instruction
|
||||||
|
// For that a reference to the string is needed. It can be created with an ImmutableStringReference.
|
||||||
|
// At last, use the method replaceInstruction to replace it at the given index startIndex.
|
||||||
|
this.replaceInstruction(
|
||||||
|
index,
|
||||||
|
BuilderInstruction21c(
|
||||||
|
Opcode.CONST_STRING,
|
||||||
|
strInstruction.registerA,
|
||||||
|
ImmutableStringReference(string)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
companion object : OptionsContainer() {
|
||||||
|
private var key1 by option(
|
||||||
|
PatchOption.StringOption(
|
||||||
|
"key1", "default", "title", "description", true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
private var key2 by option(
|
||||||
|
PatchOption.BooleanOption(
|
||||||
|
"key2", true, "title", "description" // required defaults to false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
private var key3 by option(
|
||||||
|
PatchOption.StringListOption(
|
||||||
|
"key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
private var key4 by option(
|
||||||
|
PatchOption.IntListOption(
|
||||||
|
"key4", 1, listOf(1, 2, 3), "title", "description"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
private var key5 by option(
|
||||||
|
PatchOption.StringOption(
|
||||||
|
"key5", null, "title", "description", true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
private var key6 by option(
|
||||||
|
PatchOption.PathOption(
|
||||||
|
"key6", Path("test.txt"), "title", "description", true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.revanced.patcher.usage.bytecode
|
||||||
|
import app.revanced.patcher.extensions.or
|
||||||
|
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
import org.jf.dexlib2.Opcode
|
||||||
|
|
||||||
|
@FuzzyPatternScanMethod(2)
|
||||||
|
object ExampleFingerprint : MethodFingerprint(
|
||||||
|
"V",
|
||||||
|
AccessFlags.PUBLIC or AccessFlags.STATIC,
|
||||||
|
listOf("[L"),
|
||||||
|
listOf(
|
||||||
|
Opcode.SGET_OBJECT,
|
||||||
|
null, // Testing unknown opcodes.
|
||||||
|
Opcode.INVOKE_STATIC, // This is intentionally wrong to test the Fuzzy resolver.
|
||||||
|
Opcode.RETURN_VOID
|
||||||
|
),
|
||||||
|
null
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.patcher.usage.resource.annotation
|
||||||
|
|
||||||
|
import app.revanced.patcher.annotation.Compatibility
|
||||||
|
import app.revanced.patcher.annotation.Package
|
||||||
|
|
||||||
|
@Compatibility(
|
||||||
|
[Package(
|
||||||
|
"com.example.examplePackage", arrayOf("0.0.1", "0.0.2")
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
internal annotation class ExampleResourceCompatibility
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package app.revanced.patcher.usage.resource.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.annotation.Description
|
||||||
|
import app.revanced.patcher.annotation.Name
|
||||||
|
import app.revanced.patcher.annotation.Version
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.patch.PatchResult
|
||||||
|
import app.revanced.patcher.patch.PatchResultSuccess
|
||||||
|
import app.revanced.patcher.patch.ResourcePatch
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
@Patch
|
||||||
|
@Name("example-resource-patch")
|
||||||
|
@Description("Example demonstration of a resource patch.")
|
||||||
|
@ExampleResourceCompatibility
|
||||||
|
@Version("0.0.1")
|
||||||
|
class ExampleResourcePatch : ResourcePatch {
|
||||||
|
override fun execute(context: ResourceContext): PatchResult {
|
||||||
|
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
||||||
|
val element = editor // regular DomFileEditor
|
||||||
|
.file
|
||||||
|
.getElementsByTagName("application")
|
||||||
|
.item(0) as Element
|
||||||
|
element
|
||||||
|
.setAttribute(
|
||||||
|
"exampleAttribute",
|
||||||
|
"exampleValue"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatchResultSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user