mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-11 22:06:18 +00:00
Compare commits
164 Commits
v6.4.0-dev
...
v1.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c3fbaee7a | ||
|
|
08525e9c26 | ||
|
|
5630e49663 | ||
|
|
0543122427 | ||
|
|
0873703056 | ||
|
|
1a99ecaffe | ||
|
|
6726884be5 | ||
|
|
8b4f3947f8 | ||
|
|
4d74de4061 | ||
|
|
4fbee7d255 | ||
|
|
fd9f639605 | ||
|
|
9084ccc2a2 | ||
|
|
83a8a48176 | ||
|
|
66b08f8b3a | ||
|
|
e286ba5090 | ||
|
|
e5c054ac2f | ||
|
|
cb0741d05f | ||
|
|
38556d61ab | ||
|
|
ce8021b482 | ||
|
|
243dba7751 | ||
|
|
698f759979 | ||
|
|
1701da3dde | ||
|
|
37fa9949ec | ||
|
|
ac36d19693 | ||
|
|
c245edb0c5 | ||
|
|
1f7bf3ac6c | ||
|
|
bfeeaf4435 | ||
|
|
748d0abad0 | ||
|
|
569238ab76 | ||
|
|
23197879b2 | ||
|
|
305a81793a | ||
|
|
33f9211f98 | ||
|
|
864e38c069 | ||
|
|
659e1087c9 | ||
|
|
03700ffa51 | ||
|
|
ae06d826e8 | ||
|
|
5ca5188fc2 | ||
|
|
f88c11820d | ||
|
|
93e81ff047 | ||
|
|
d49df10a3c | ||
|
|
04b49b8b66 | ||
|
|
5ddc63f979 | ||
|
|
82b1e66d54 | ||
|
|
fd630cd429 | ||
|
|
f4a47d4dc8 | ||
|
|
3bfc24fc16 | ||
|
|
25bba2c1d8 | ||
|
|
4dea27e831 | ||
|
|
a0d6d46217 | ||
|
|
643a14e664 | ||
|
|
355e6d82cc | ||
|
|
df7503b47b | ||
|
|
a01dded092 | ||
|
|
9ae95174e6 | ||
|
|
e161f7fea4 | ||
|
|
200e3c9fdb | ||
|
|
f0f34031dd | ||
|
|
560c485ab0 | ||
|
|
cc5a414692 | ||
|
|
c2a334eb3f | ||
|
|
1b2fbbca26 | ||
|
|
4458141d6d | ||
|
|
8544fc4cbc | ||
|
|
a492808021 | ||
|
|
0204eee79e | ||
|
|
4022b8b847 | ||
|
|
8daf877fac | ||
|
|
7d38bb0baa | ||
|
|
5f71a342ac | ||
|
|
866b03af21 | ||
|
|
4c1a42b216 | ||
|
|
264989f488 | ||
|
|
4281546f69 | ||
|
|
af4f2396c7 | ||
|
|
147195647c | ||
|
|
433914feda | ||
|
|
622138736d | ||
|
|
aed4fd9a3c | ||
|
|
32e645850d | ||
|
|
e45fc02aae | ||
|
|
e0d29cf450 | ||
|
|
2b888e381c | ||
|
|
f72dd68ec5 | ||
|
|
3b68d5c65e | ||
|
|
eed1cfda7b | ||
|
|
8b70bb4290 | ||
|
|
dbda641d0c | ||
|
|
5ae5e98f1f | ||
|
|
1ba40ab1cb | ||
|
|
e9c119ebb1 | ||
|
|
1bd6d1d5b8 | ||
|
|
4e7378bd79 | ||
|
|
28ed4793e3 | ||
|
|
312235b194 | ||
|
|
6ab21e5891 | ||
|
|
db8d1150c3 | ||
|
|
8f778f38fe | ||
|
|
88a6a27302 | ||
|
|
a9e4e8ac32 | ||
|
|
d5e694c306 | ||
|
|
dde0a22642 | ||
|
|
9a67aa3ff4 | ||
|
|
e69708f21e | ||
|
|
c49071aff7 | ||
|
|
d15240d033 | ||
|
|
6767c8fbc1 | ||
|
|
4543b36616 | ||
|
|
ec6d462ade | ||
|
|
84bc7e0dc7 | ||
|
|
6ad51aad9a | ||
|
|
b711b8001e | ||
|
|
12c10d8c64 | ||
|
|
05e44007d8 | ||
|
|
dbafe2ab37 | ||
|
|
45a885dbde | ||
|
|
78235d1abe | ||
|
|
aec5eeb597 | ||
|
|
d98c9eeb30 | ||
|
|
f8e978af88 | ||
|
|
86cb053566 | ||
|
|
c1ccb70de4 | ||
|
|
bb42fa3c6f | ||
|
|
2d3c61113d | ||
|
|
6bc4e7eab7 | ||
|
|
be51f42710 | ||
|
|
fa0412985c | ||
|
|
0048788dd0 | ||
|
|
47eb493f54 | ||
|
|
6b1337e4fc | ||
|
|
f4589db3a9 | ||
|
|
1af31b2aa3 | ||
|
|
14f7667156 | ||
|
|
cd57a8c9a0 | ||
|
|
0d3beb353d | ||
|
|
ddef338631 | ||
|
|
fc4b673087 | ||
|
|
8d1bb5f3d9 | ||
|
|
c8a017a4c0 | ||
|
|
51fb59a43c | ||
|
|
a78715133c | ||
|
|
e8f6973938 | ||
|
|
3cb1e01587 | ||
|
|
cb4ee207e1 | ||
|
|
ca6b94d943 | ||
|
|
6cdb6887d4 | ||
|
|
ab6453ca8a | ||
|
|
e8182c17ad | ||
|
|
49beec9fc6 | ||
|
|
3ab42a932c | ||
|
|
4d98cbc9e8 | ||
|
|
87bbde5e06 | ||
|
|
8db8893ab1 | ||
|
|
00c6ab7faf | ||
|
|
460d62a24c | ||
|
|
89e4b9f762 | ||
|
|
a8fd7c00c3 | ||
|
|
1769132a9e | ||
|
|
6c0f0823c9 | ||
|
|
23e897a7a9 | ||
|
|
7e67daf878 | ||
|
|
593c83f29f | ||
|
|
72e123dd01 | ||
|
|
599a401ed9 | ||
|
|
3f8500b059 |
41
.github/workflows/release.yml
vendored
Normal file
41
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew build
|
||||
- name: Setup semantic-release
|
||||
run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog gradle-semantic-release-plugin -D
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release
|
||||
15
.idea/git_toolbox_prj.xml
generated
Normal file
15
.idea/git_toolbox_prj.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxProjectSettings">
|
||||
<option name="commitMessageIssueKeyValidationOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
<option name="commitMessageValidationEnabledOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="UnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CommitMessageInspectionProfile">
|
||||
<profile version="1.0">
|
||||
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
|
||||
25
.releaserc
Normal file
25
.releaserc
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
198
CHANGELOG.md
Normal file
198
CHANGELOG.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# [1.0.0-dev.12](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.11...v1.0.0-dev.12) (2022-05-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* using old instance of `Androlib` when saving ([5630e49](https://github.com/revanced/revanced-patcher/commit/5630e4966310311cdfd53e2ba128255047626adc))
|
||||
|
||||
# [1.0.0-dev.11](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.10...v1.0.0-dev.11) (2022-05-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* `PatchLoader` ([1a99eca](https://github.com/revanced/revanced-patcher/commit/1a99ecaffe5e55977655316e68b014fdeba374a1))
|
||||
* use annotations instead of metadata objects ([6726884](https://github.com/revanced/revanced-patcher/commit/6726884be5af56b6856749e73fb9f4f97559854a))
|
||||
|
||||
# [1.0.0-dev.10](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.9...v1.0.0-dev.10) (2022-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* qualifying `Element` with wrong package ([4d74de4](https://github.com/revanced/revanced-patcher/commit/4d74de4061f26c0d7c17fabd849051b429d86033))
|
||||
|
||||
# [1.0.0-dev.9](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.8...v1.0.0-dev.9) (2022-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `compareSignatureToMethod` not matching correctly in case opcodes are null ([5ae5e98](https://github.com/revanced/revanced-patcher/commit/5ae5e98f1f8e174d800bcc75723e1ed965d66196))
|
||||
* `ConcurrentModificationException` while iterating through `proxies` and modifying it ([bfeeaf4](https://github.com/revanced/revanced-patcher/commit/bfeeaf443549c9a43279d83a0628c061a382beb9))
|
||||
* `PackageMetadata` ([305a817](https://github.com/revanced/revanced-patcher/commit/305a81793a9a04fe4e8969f2d3b591b0f01e3b63))
|
||||
* `replaceWith` not replacing classes with used class proxies ([f0f3403](https://github.com/revanced/revanced-patcher/commit/f0f34031dd4e618223f016f7c427d7c93ab8456a))
|
||||
* adding existing classes to the patchers cache ([4281546](https://github.com/revanced/revanced-patcher/commit/4281546f69225ee90ec4c003f4313df41edf71a6))
|
||||
* always return PatchResultSuccess on patch success ([866b03a](https://github.com/revanced/revanced-patcher/commit/866b03af217ad97dd2755bfdc0ffe5bcf723c949))
|
||||
* applying no patches throwing error ([f88c118](https://github.com/revanced/revanced-patcher/commit/f88c11820dbdc0d1d52a49c9bcdb4f7caa9eb6eb))
|
||||
* applyPatches not returning successful patches ([8b70bb4](https://github.com/revanced/revanced-patcher/commit/8b70bb42909434a5e59315502f6d54d7c7691f18))
|
||||
* Classes not being written properly because of array shifting ([1471956](https://github.com/revanced/revanced-patcher/commit/147195647c3990ab78ba95e4b3000650e718b713))
|
||||
* failing tests temporarily ([66b08f8](https://github.com/revanced/revanced-patcher/commit/66b08f8b3a8f31844c7e7bab4df4243521d4a431))
|
||||
* fix classes having multiple instances of fields ([b711b80](https://github.com/revanced/revanced-patcher/commit/b711b8001e4845857fa6cc71b107f1c553b31e80))
|
||||
* fix classes having multiple method instances ([12c10d8](https://github.com/revanced/revanced-patcher/commit/12c10d8c64422c4534c23467e367707e3b953f82))
|
||||
* Fixed writer & signature resolver, improved tests & speed, minor refactoring ([bb42fa3](https://github.com/revanced/revanced-patcher/commit/bb42fa3c6f59b78a7223fc70edbe598ec181ee37))
|
||||
* fuzzy resolver warning params were turned around ([d49df10](https://github.com/revanced/revanced-patcher/commit/d49df10a3ca6b472ce4a32d10cfe787ca243d47b))
|
||||
* incorrect pattern offset ([03700ff](https://github.com/revanced/revanced-patcher/commit/03700ffa519e5f20b1a0d0ffe68f3fb504351ee5))
|
||||
* make `methodMetadata` nullable in `MethodSignatureMetadata` ([864e38c](https://github.com/revanced/revanced-patcher/commit/864e38c06906a9e29271fe383d51a8ec6594a46c))
|
||||
* make warnings nullable instead of lateinit ([04b49b8](https://github.com/revanced/revanced-patcher/commit/04b49b8b664e45e64e9561eca3353ffdeda91187))
|
||||
* match to correct signature method parameters ([c49071a](https://github.com/revanced/revanced-patcher/commit/c49071aff78245f27c98a4760b361c30aa6340bc))
|
||||
* MethodSignature#resolved throwing an exception ([82b1e66](https://github.com/revanced/revanced-patcher/commit/82b1e66d54bed1e4c335e0515b7ff3ec901fa6f8))
|
||||
* Move proxy package out of cache package ([6bc4e7e](https://github.com/revanced/revanced-patcher/commit/6bc4e7eab742f5796f3041332c70495e3f993c9b))
|
||||
* null check causing an exception ([560c485](https://github.com/revanced/revanced-patcher/commit/560c485ab08b08a213b58704b11b1e2f5f625080))
|
||||
* Patcher not writing resolved methods ([d15240d](https://github.com/revanced/revanced-patcher/commit/d15240d0330a63c4b568fc5de3de861b8046cba4))
|
||||
* reaching all constructors not possible ([37fa994](https://github.com/revanced/revanced-patcher/commit/37fa9949ec84ffd277f32b1cd554e92be41d35e4))
|
||||
* remove leftover debug code ([4458141](https://github.com/revanced/revanced-patcher/commit/4458141d6d2e1b015c0d70a6e65e6c32a3cf17dc))
|
||||
* return mutable set of classes ([84bc7e0](https://github.com/revanced/revanced-patcher/commit/84bc7e0dc76f0732613383accb803f2c52da98ac))
|
||||
* returning failure on success ([3b68d5c](https://github.com/revanced/revanced-patcher/commit/3b68d5c65ec3082d1aa48525b4ee2a4163895a3b))
|
||||
* Search method map for existing class proxy ([d5e694c](https://github.com/revanced/revanced-patcher/commit/d5e694c306a47f47b8d1078b5c9f8a742445cf7e))
|
||||
* string signature in `SignatureResolver` ([ac36d19](https://github.com/revanced/revanced-patcher/commit/ac36d19693390db8f404ed30963aefb2fb7519e0))
|
||||
* Suppress unused for addFiles ([a0d6d46](https://github.com/revanced/revanced-patcher/commit/a0d6d462170552929039d71eafa813fdfde215cb))
|
||||
* throwing in case the opcode patterns do not match ([f72dd68](https://github.com/revanced/revanced-patcher/commit/f72dd68ec575ee0926ee668911ebb6f85b75f7d1))
|
||||
* use Array instead of Iterable for methodParameters ([312235b](https://github.com/revanced/revanced-patcher/commit/312235b194cac01ddc3f03ecff32c7de4e48c29c))
|
||||
* write all classes ([6ad51aa](https://github.com/revanced/revanced-patcher/commit/6ad51aad9a94d8dd5afb5e270138ef7161ccfb07))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* bump multidexlib2 to 2.5.2.r2 ([32e6458](https://github.com/revanced/revanced-patcher/commit/32e645850d4cc74aa708984da03ae1606e696d20))
|
||||
* Change all references from Array to Iterable ([264989f](https://github.com/revanced/revanced-patcher/commit/264989f48804ed637469436acf8165ac4b7be383))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `MethodWalker` ([659e108](https://github.com/revanced/revanced-patcher/commit/659e1087c9e7a33e04cd7eb728c01ed946335810))
|
||||
* add `p` naming scheme to smali compiler ([38556d6](https://github.com/revanced/revanced-patcher/commit/38556d61ab192dfa84083d935ee3e9eee5450d06))
|
||||
* add extensions for cloning methods ([df7503b](https://github.com/revanced/revanced-patcher/commit/df7503b47b1e2162d6ab666f8586c633c314016f))
|
||||
* add findClass method with className ([78235d1](https://github.com/revanced/revanced-patcher/commit/78235d1abe267e6aaa086662ad69af7132b8ff74))
|
||||
* Add first tests ([6767c8f](https://github.com/revanced/revanced-patcher/commit/6767c8fbc15ea18a61db53e1472483632077f62a))
|
||||
* add fuzzy resolver ([a492808](https://github.com/revanced/revanced-patcher/commit/a4928080217451017a99cf158fd5cc9d650a5a9e))
|
||||
* add immutableMethod ([eed1cfd](https://github.com/revanced/revanced-patcher/commit/eed1cfda7b89f03f4c61ac4401707e1a12e6efb3))
|
||||
* add inline smali compiler ([dbafe2a](https://github.com/revanced/revanced-patcher/commit/dbafe2ab37b25480f3e218d94ced5af2e56cba68))
|
||||
* add missing test for fields ([4022b8b](https://github.com/revanced/revanced-patcher/commit/4022b8b847e8767ace0da3f98ad72ab61a4c242b))
|
||||
* add or extension for AccessFlags ([aec5eeb](https://github.com/revanced/revanced-patcher/commit/aec5eeb597f0e9968b43efa228c96e83175e031c))
|
||||
* Add patch metadata ([8544fc4](https://github.com/revanced/revanced-patcher/commit/8544fc4cbcb5d7c1ac0f6fcae52882a00d2bacf5)), closes [ReVancedTeam/revanced-patches#1](https://github.com/ReVancedTeam/revanced-patches/issues/1)
|
||||
* Add warnings for Fuzzy resolver ([643a14e](https://github.com/revanced/revanced-patcher/commit/643a14e664c7ff86580da683eaff9c486884ee2c))
|
||||
* allow classes to be overwritten in addFiles and resolve signatures when applyPatches is called ([5f71a34](https://github.com/revanced/revanced-patcher/commit/5f71a342ac9c6aa64a4983156f595ae0832c30e8))
|
||||
* Allow unknown opcodes using `null` ([f4a47d4](https://github.com/revanced/revanced-patcher/commit/f4a47d4dc893bb511ca2087a1a63bfc35888663f))
|
||||
* Finish first patcher test ([a9e4e8a](https://github.com/revanced/revanced-patcher/commit/a9e4e8ac3203bdd62abcd1e366f08a2269919571))
|
||||
* Improve `SignatureResolver` ([88a6a27](https://github.com/revanced/revanced-patcher/commit/88a6a2730296883e191543c2666f39f24c05d74d))
|
||||
* migrate to dexlib ([be51f42](https://github.com/revanced/revanced-patcher/commit/be51f42710c1489ef4405700e56ffecee5e6552f))
|
||||
* Minor refactor and return proxy, if class has been proxied already ([2d3c611](https://github.com/revanced/revanced-patcher/commit/2d3c61113dc9b76c43e93928ba11026fe0ad444e))
|
||||
* properly manage `ClassProxy` & add `ProxyBackedClassList` ([2319787](https://github.com/revanced/revanced-patcher/commit/23197879b20906aac7563e5f8107305edd7ccb1b))
|
||||
* remaining mutable `EncodedValue` classes ([7d38bb0](https://github.com/revanced/revanced-patcher/commit/7d38bb0baaeabade6a9e64d97e2dd6c20edd153f))
|
||||
* string signature ([#22](https://github.com/revanced/revanced-patcher/issues/22)) ([c245edb](https://github.com/revanced/revanced-patcher/commit/c245edb0c5317c1bb884ea315a1a04b720f20dd5))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* depend on `androlib` instead of `ApkDecoder` ([e5c054a](https://github.com/revanced/revanced-patcher/commit/e5c054ac2f68b00ac123a45ed56b9f150332a82d))
|
||||
* do not resolve empty signatures list ([1f7bf3a](https://github.com/revanced/revanced-patcher/commit/1f7bf3ac6c77a71abd687f2ff6f7306a40654a1b))
|
||||
* lazy-ify all mutable clones ([05e4400](https://github.com/revanced/revanced-patcher/commit/05e44007d81399791aa1bab1eead66b7ff662043))
|
||||
* optimize indexOf call away ([f8e978a](https://github.com/revanced/revanced-patcher/commit/f8e978af888255d9c104a8275be1d9b091af3f96))
|
||||
* use Set instead of List since there are no dupes ([6221387](https://github.com/revanced/revanced-patcher/commit/622138736dca6c0161171330801b7b5666594ec7))
|
||||
* use String List and compare instead of any lambda ([aed4fd9](https://github.com/revanced/revanced-patcher/commit/aed4fd9a3c9e7f96c1e2c54b831c3fe7d3d720a2))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* AccessFlag extensions not working with IDE ([e161f7f](https://github.com/revanced/revanced-patcher/commit/e161f7fea449883b7ac0fb436ed4f7f2ff78af62))
|
||||
* previous commits check for dupes in dexFile, not cache ([433914f](https://github.com/revanced/revanced-patcher/commit/433914feda3066102a073d6b3bc457d0fae87911))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* arrayOf has to be changed to listOf.
|
||||
* Method signature of Patcher#save() was changed to comply with the changes of multidexlib2.
|
||||
* Removed usage of ASM library
|
||||
|
||||
# [1.0.0-dev.8](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.7...v1.0.0-dev.8) (2022-03-24)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* check type instead of class ([47eb493](https://github.com/ReVancedTeam/revanced-patcher/commit/47eb493f5425dc27a4d6e79e6b02a36ef760e8da))
|
||||
|
||||
# [1.0.0-dev.7](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.6...v1.0.0-dev.7) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MethodResolver:** fix cd57a8c9a0db7e3ae5ad0bca202e5955930319ab ([1af31b2](https://github.com/ReVancedTeam/revanced-patcher/commit/1af31b2aa3772a7473c04d27bf835c8eae13438d))
|
||||
|
||||
# [1.0.0-dev.6](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.5...v1.0.0-dev.6) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MethodResolver:** strip labels nodes so opcode patterns match ([cd57a8c](https://github.com/ReVancedTeam/revanced-patcher/commit/cd57a8c9a0db7e3ae5ad0bca202e5955930319ab))
|
||||
|
||||
# [1.0.0-dev.5](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.4...v1.0.0-dev.5) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MethodResolver:** strip labels and line numbers so opcode patterns match ([8d1bb5f](https://github.com/ReVancedTeam/revanced-patcher/commit/8d1bb5f3d9da544cf6e3e3848bfcc56327cde810))
|
||||
|
||||
# [1.0.0-dev.4](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.3...v1.0.0-dev.4) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* give ClassWriter a ClassReader for symtable ([e8f6973](https://github.com/ReVancedTeam/revanced-patcher/commit/e8f6973938c70002f04a86f329aa5b134f6ef649))
|
||||
|
||||
# [1.0.0-dev.3](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.2...v1.0.0-dev.3) (2022-03-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add SafeClassWriter ([ca6b94d](https://github.com/ReVancedTeam/revanced-patcher/commit/ca6b94d943b7067aae87a4e282cfb323811c0462))
|
||||
|
||||
# [1.0.0-dev.2](https://github.com/ReVancedTeam/revanced-patcher/compare/v1.0.0-dev.1...v1.0.0-dev.2) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* set marklimit to Integer.MAX_VALUE ([ab6453c](https://github.com/ReVancedTeam/revanced-patcher/commit/ab6453ca8a02af70da4468c1a63c68dde4d392ef))
|
||||
|
||||
# 1.0.0-dev.1 (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid ignoring test resources (fixes [#1](https://github.com/ReVancedTeam/revanced-patcher/issues/1)) ([d5a3c76](https://github.com/ReVancedTeam/revanced-patcher/commit/d5a3c76389ba902c22ddc8b7ba1a110b7ff852df))
|
||||
* current must be calculated after increment ([5f12bab](https://github.com/ReVancedTeam/revanced-patcher/commit/5f12bab5df97fbe6e2e62c1bf2814a2e682ab4f3))
|
||||
* **gradle:** publish source and javadocs ([87bbde5](https://github.com/ReVancedTeam/revanced-patcher/commit/87bbde5e06d038d8f6ddaac391e1db397f5a5590))
|
||||
* **Io:** fix finding classes by name ([460d62a](https://github.com/ReVancedTeam/revanced-patcher/commit/460d62a24c4cad05691c4b269c2faeda47fee3b7))
|
||||
* **Io:** JAR loading and saving ([#8](https://github.com/ReVancedTeam/revanced-patcher/issues/8)) ([4d98cbc](https://github.com/ReVancedTeam/revanced-patcher/commit/4d98cbc9e8fe1e39b3d9d4185b3c5b4882093af6))
|
||||
* nullable signature members ([#10](https://github.com/ReVancedTeam/revanced-patcher/issues/10)) ([8db8893](https://github.com/ReVancedTeam/revanced-patcher/commit/8db8893ab1bda55f11cc75db55c7c1a38f1d1b16))
|
||||
* Patch should have access to the Cache ([6c0f082](https://github.com/ReVancedTeam/revanced-patcher/commit/6c0f0823c91dc643dd80205b1e840e59827bee06))
|
||||
* remove broken code ([0e72a6e](https://github.com/ReVancedTeam/revanced-patcher/commit/0e72a6e85ff9a6035510680fc5e33ab0cd14144f))
|
||||
* set index for insertAt to 0 by default ([1769132](https://github.com/ReVancedTeam/revanced-patcher/commit/1769132a9e29cf3a0c5ae0917209c83c138c0216))
|
||||
* workflow on dev branch ([7e67daf](https://github.com/ReVancedTeam/revanced-patcher/commit/7e67daf8789c534bed0091a3975776eb95039acc))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* convert Patch to abstract class ([23e897a](https://github.com/ReVancedTeam/revanced-patcher/commit/23e897a7a9125f4ac4266263e7dd94fe63a0bfa1))
|
||||
* Optimize Signature class ([#11](https://github.com/ReVancedTeam/revanced-patcher/issues/11)) ([49beec9](https://github.com/ReVancedTeam/revanced-patcher/commit/49beec9fc6eee6ccf52a6185761a200a6ed2b16e))
|
||||
* Rename `net.revanced` to `app.revanced` ([3ab42a9](https://github.com/ReVancedTeam/revanced-patcher/commit/3ab42a932c8d5027d554106dfe8e1299ebc1ac7f))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `findParentMethod` utility method ([#4](https://github.com/ReVancedTeam/revanced-patcher/issues/4)) ([00c6ab7](https://github.com/ReVancedTeam/revanced-patcher/commit/00c6ab7fafe2a59dec0052cc5b7d1d16939076b2))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Array<Int> was changed to IntArray. This breaks existing patches.
|
||||
* Package name was changed from "net.revanced" to "app.revanced"
|
||||
* Method signature of execute() was changed to include the cache, this will break existing implementations of the Patch class.
|
||||
* Patch class is now an abstract class. You must implement it. You can use anonymous implements, like done in the tests.
|
||||
@@ -1,41 +1,66 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.6.10"
|
||||
kotlin("jvm") version "1.6.20"
|
||||
java
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "net.revanced"
|
||||
group = "app.revanced"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
||||
credentials {
|
||||
// DO NOT set these variables in the project's gradle.properties.
|
||||
// Instead, you should set them in:
|
||||
// Windows: %homepath%\.gradle\gradle.properties
|
||||
// Linux: ~/.gradle/gradle.properties
|
||||
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") // DO NOT CHANGE!
|
||||
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation("org.ow2.asm:asm:9.2")
|
||||
implementation("org.ow2.asm:asm-util:9.2")
|
||||
implementation("org.ow2.asm:asm-tree:9.2")
|
||||
implementation("org.ow2.asm:asm-commons:9.2")
|
||||
implementation("io.github.microutils:kotlin-logging:2.1.21")
|
||||
testImplementation("ch.qos.logback:logback-classic:1.2.11") // use your own logger!
|
||||
|
||||
api("org.apktool:apktool-lib:2.6.1")
|
||||
api("app.revanced:multidexlib2:2.5.2.r2")
|
||||
api("org.smali:smali:2.5.2")
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
implementation(kotlin("reflect"))
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("PASSED", "SKIPPED", "FAILED")
|
||||
tasks {
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("PASSED", "SKIPPED", "FAILED")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
val isGitHubCI = System.getenv("GITHUB_ACTOR") != null
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/ReVancedTeam/revanced-patcher")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
if (isGitHubCI) {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
publications {
|
||||
@@ -43,4 +68,4 @@ publishing {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
kotlin.code.style=official
|
||||
version=1.0.0
|
||||
kotlin.code.style = official
|
||||
version = 1.0.0-dev.12
|
||||
|
||||
223
src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
223
src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
@@ -0,0 +1,223 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.data.PatcherData
|
||||
import app.revanced.patcher.data.base.Data
|
||||
import app.revanced.patcher.data.implementation.findIndexed
|
||||
import app.revanced.patcher.extensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import app.revanced.patcher.patch.implementation.BytecodePatch
|
||||
import app.revanced.patcher.patch.implementation.ResourcePatch
|
||||
import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
import app.revanced.patcher.signature.implementation.method.resolver.MethodSignatureResolver
|
||||
import app.revanced.patcher.util.ListBackedSet
|
||||
import brut.androlib.Androlib
|
||||
import brut.androlib.meta.UsesFramework
|
||||
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.ClassDef
|
||||
import org.jf.dexlib2.iface.DexFile
|
||||
import org.jf.dexlib2.writer.io.MemoryDataStore
|
||||
import java.io.File
|
||||
|
||||
val NAMER = BasicDexFileNamer()
|
||||
|
||||
/**
|
||||
* The ReVanced Patcher.
|
||||
* @param inputFile The input file (usually an apk file).
|
||||
* @param resourceCacheDirectory Directory to cache resources.
|
||||
* @param patchResources Weather to use the resource patcher. Resources will still need to be decoded.
|
||||
*/
|
||||
class Patcher(
|
||||
inputFile: File,
|
||||
// TODO: maybe a file system in memory is better. Could cause high memory usage.
|
||||
private val resourceCacheDirectory: String,
|
||||
private val patchResources: Boolean = false
|
||||
) {
|
||||
val packageVersion: String
|
||||
val packageName: String
|
||||
|
||||
private val usesFramework: UsesFramework
|
||||
private val patcherData: PatcherData
|
||||
private val opcodes: Opcodes
|
||||
private var signaturesResolved = false
|
||||
|
||||
|
||||
init {
|
||||
val extFileInput = ExtFile(inputFile)
|
||||
val outDir = File(resourceCacheDirectory)
|
||||
|
||||
if (outDir.exists()) outDir.deleteRecursively()
|
||||
outDir.mkdir()
|
||||
|
||||
val androlib = Androlib()
|
||||
|
||||
val resourceTable = androlib.getResTable(extFileInput, true)
|
||||
// 1. decode resources to cache directory
|
||||
androlib.decodeManifestWithResources(extFileInput, outDir, resourceTable)
|
||||
androlib.decodeResourcesFull(extFileInput, outDir, resourceTable)
|
||||
|
||||
// 2. read framework ids from the resource table
|
||||
usesFramework = UsesFramework()
|
||||
usesFramework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
|
||||
|
||||
// 2. read package info
|
||||
packageName = resourceTable.packageOriginal
|
||||
packageVersion = resourceTable.versionInfo.versionName
|
||||
|
||||
// read dex files
|
||||
val dexFile = MultiDexIO.readDexFile(true, inputFile, NAMER, null, null)
|
||||
opcodes = dexFile.opcodes
|
||||
|
||||
// save to patcher data
|
||||
patcherData = PatcherData(dexFile.classes.toMutableList(), resourceCacheDirectory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional dex file container to the patcher.
|
||||
* @param files The dex file containers to add to the patcher.
|
||||
* @param allowedOverwrites A list of class types that are allowed to be overwritten.
|
||||
* @param throwOnDuplicates If this is set to true, the patcher will throw an exception if a duplicate class has been found.
|
||||
*/
|
||||
fun addFiles(
|
||||
files: Iterable<File>,
|
||||
allowedOverwrites: Iterable<String> = emptyList(),
|
||||
throwOnDuplicates: Boolean = false
|
||||
) {
|
||||
for (file in files) {
|
||||
val dexFile = MultiDexIO.readDexFile(true, file, NAMER, null, null)
|
||||
for (classDef in dexFile.classes) {
|
||||
val e = patcherData.bytecodeData.classes.internalClasses.findIndexed { it.type == classDef.type }
|
||||
if (e != null) {
|
||||
if (throwOnDuplicates) {
|
||||
throw Exception("Class ${classDef.type} has already been added to the patcher.")
|
||||
}
|
||||
val (_, idx) = e
|
||||
if (allowedOverwrites.contains(classDef.type)) {
|
||||
patcherData.bytecodeData.classes.internalClasses[idx] = classDef
|
||||
}
|
||||
continue
|
||||
}
|
||||
patcherData.bytecodeData.classes.internalClasses.add(classDef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the patched dex file.
|
||||
*/
|
||||
fun save(): Map<String, MemoryDataStore> {
|
||||
val newDexFile = object : DexFile {
|
||||
override fun getClasses(): Set<ClassDef> {
|
||||
patcherData.bytecodeData.classes.applyProxies()
|
||||
return ListBackedSet(patcherData.bytecodeData.classes.internalClasses)
|
||||
}
|
||||
|
||||
override fun getOpcodes(): Opcodes {
|
||||
return this@Patcher.opcodes
|
||||
}
|
||||
}
|
||||
|
||||
// build modified resources
|
||||
if (patchResources) {
|
||||
val extDir = ExtFile(resourceCacheDirectory)
|
||||
|
||||
// TODO: figure out why a new instance of Androlib is necessary here
|
||||
Androlib().buildResources(extDir, usesFramework)
|
||||
}
|
||||
|
||||
// write dex modified files
|
||||
val output = mutableMapOf<String, MemoryDataStore>()
|
||||
MultiDexIO.writeDexFile(
|
||||
true, -1, // core count
|
||||
output, NAMER, newDexFile,
|
||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
||||
null
|
||||
)
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a patch to the patcher.
|
||||
* @param patches The patches to add.
|
||||
*/
|
||||
fun addPatches(patches: Iterable<Patch<Data>>) {
|
||||
patcherData.patches.addAll(patches)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all signatures.
|
||||
*/
|
||||
fun resolveSignatures(): List<MethodSignature> {
|
||||
val signatures = buildList {
|
||||
for (patch in patcherData.patches) {
|
||||
if (patch !is BytecodePatch) continue
|
||||
this.addAll(patch.signatures)
|
||||
}
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
MethodSignatureResolver(patcherData.bytecodeData.classes.internalClasses, signatures).resolve(patcherData)
|
||||
signaturesResolved = true
|
||||
return signatures
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply patches loaded into the patcher.
|
||||
* @param stopOnError If true, the patches will stop on the first error.
|
||||
* @return A map of [PatchResultSuccess]. If the [Patch] was successfully applied,
|
||||
* [PatchResultSuccess] will always be returned to the wrapping Result object.
|
||||
* If the [Patch] failed to apply, an Exception will always be returned to the wrapping Result object.
|
||||
*/
|
||||
fun applyPatches(
|
||||
stopOnError: Boolean = false,
|
||||
callback: (String) -> Unit = {}
|
||||
): Map<String, Result<PatchResultSuccess>> {
|
||||
if (!signaturesResolved) {
|
||||
resolveSignatures()
|
||||
}
|
||||
return buildMap {
|
||||
for (patch in patcherData.patches) {
|
||||
val resourcePatch = patch is ResourcePatch
|
||||
if (!patchResources && resourcePatch) continue
|
||||
|
||||
val patchNameAnnotation = patch::class.java.findAnnotationRecursively(Name::class.java)
|
||||
|
||||
patchNameAnnotation?.let {
|
||||
callback(it.name)
|
||||
}
|
||||
|
||||
val result: Result<PatchResultSuccess> = try {
|
||||
val data = if (resourcePatch) {
|
||||
patcherData.resourceData
|
||||
} else {
|
||||
patcherData.bytecodeData
|
||||
}
|
||||
|
||||
val pr = patch.execute(data)
|
||||
|
||||
if (pr.isSuccess()) {
|
||||
Result.success(pr.success()!!)
|
||||
} else {
|
||||
Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
|
||||
patchNameAnnotation?.let {
|
||||
this[patchNameAnnotation.name] = result
|
||||
}
|
||||
|
||||
if (result.isFailure && stopOnError) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package app.revanced.patcher.annotation
|
||||
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
|
||||
/**
|
||||
* Annotation to constrain a [Patch] or [MethodSignature] to compatible packages.
|
||||
* @param compatiblePackages A list of packages a [Patch] or [MethodSignature] is compatible with.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
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] or [MethodSignature]is compatible with.
|
||||
*/
|
||||
@Target()
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
annotation class Package(
|
||||
val name: String,
|
||||
val versions: Array<String>
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package app.revanced.patcher.annotation
|
||||
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
|
||||
/**
|
||||
* Annotation to name a [Patch] or [MethodSignature].
|
||||
* @param name A suggestive name for the [Patch] or [MethodSignature].
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
annotation class Name(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotation to describe a [Patch] or [MethodSignature].
|
||||
* @param description A description for the [Patch] or [MethodSignature].
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
annotation class Description(
|
||||
val description: String,
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Annotation to version a [Patch] or [MethodSignature].
|
||||
* @param version The version of a [Patch] or [MethodSignature].
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
annotation class Version(
|
||||
val version: String,
|
||||
)
|
||||
18
src/main/kotlin/app/revanced/patcher/data/PatcherData.kt
Normal file
18
src/main/kotlin/app/revanced/patcher/data/PatcherData.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package app.revanced.patcher.data
|
||||
|
||||
import app.revanced.patcher.data.base.Data
|
||||
import app.revanced.patcher.data.implementation.BytecodeData
|
||||
import app.revanced.patcher.data.implementation.ResourceData
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import org.jf.dexlib2.iface.ClassDef
|
||||
import java.io.File
|
||||
|
||||
internal data class PatcherData(
|
||||
val internalClasses: MutableList<ClassDef>,
|
||||
val resourceCacheDirectory: String
|
||||
) {
|
||||
internal val patches = mutableListOf<Patch<Data>>()
|
||||
|
||||
internal val bytecodeData = BytecodeData(patches, internalClasses)
|
||||
internal val resourceData = ResourceData(File(resourceCacheDirectory))
|
||||
}
|
||||
9
src/main/kotlin/app/revanced/patcher/data/base/Data.kt
Normal file
9
src/main/kotlin/app/revanced/patcher/data/base/Data.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package app.revanced.patcher.data.base
|
||||
|
||||
import app.revanced.patcher.data.implementation.BytecodeData
|
||||
import app.revanced.patcher.data.implementation.ResourceData
|
||||
|
||||
/**
|
||||
* Constraint interface for [BytecodeData] and [ResourceData]
|
||||
*/
|
||||
interface Data
|
||||
@@ -0,0 +1,86 @@
|
||||
package app.revanced.patcher.data.implementation
|
||||
|
||||
import app.revanced.patcher.data.base.Data
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import app.revanced.patcher.patch.implementation.BytecodePatch
|
||||
import app.revanced.patcher.signature.implementation.method.resolver.SignatureResolverResult
|
||||
import app.revanced.patcher.util.ProxyBackedClassList
|
||||
import app.revanced.patcher.util.method.MethodWalker
|
||||
import org.jf.dexlib2.iface.ClassDef
|
||||
import org.jf.dexlib2.iface.Method
|
||||
|
||||
class BytecodeData(
|
||||
// FIXME: ugly solution due to design.
|
||||
// It does not make sense for a BytecodeData instance to have access to the patches
|
||||
private val patches: List<Patch<Data>>,
|
||||
internalClasses: MutableList<ClassDef>
|
||||
) : Data {
|
||||
val classes = ProxyBackedClassList(internalClasses)
|
||||
|
||||
/**
|
||||
* Find a class by a given class name
|
||||
* @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
|
||||
* @return A proxy for the first class that matches the predicate
|
||||
*/
|
||||
fun findClass(predicate: (ClassDef) -> Boolean): app.revanced.patcher.util.proxy.ClassProxy? {
|
||||
// if we already proxied the class matching the predicate...
|
||||
for (patch in patches) {
|
||||
if (patch !is BytecodePatch) continue
|
||||
for (signature in patch.signatures) {
|
||||
val result = signature.result
|
||||
result ?: continue
|
||||
|
||||
if (predicate(result.definingClassProxy.immutableClass)) return result.definingClassProxy // ...then return that proxy
|
||||
}
|
||||
}
|
||||
// else resolve the class to a proxy and return it, if the predicate is matching a class
|
||||
return classes.find(predicate)?.let {
|
||||
proxy(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MethodMap : LinkedHashMap<String, SignatureResolverResult>() {
|
||||
override fun get(key: String): SignatureResolverResult {
|
||||
return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache")
|
||||
}
|
||||
}
|
||||
|
||||
internal class MethodNotFoundException(s: String) : Exception(s)
|
||||
|
||||
internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
|
||||
for (element in this) {
|
||||
if (predicate(element)) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun BytecodeData.toMethodWalker(startMethod: Method): MethodWalker {
|
||||
return MethodWalker(this, startMethod)
|
||||
}
|
||||
|
||||
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
|
||||
for ((index, element) in this.withIndex()) {
|
||||
if (predicate(element)) {
|
||||
return element to index
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun BytecodeData.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.proxies.add(proxy)
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package app.revanced.patcher.data.implementation
|
||||
|
||||
import app.revanced.patcher.data.base.Data
|
||||
import org.w3c.dom.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import javax.xml.XMLConstants
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
|
||||
class ResourceData(private val resourceCacheDirectory: File) : Data {
|
||||
private fun resolve(path: String) = resourceCacheDirectory.resolve(path)
|
||||
|
||||
fun forEach(action: (File) -> Unit) = resourceCacheDirectory.walkTopDown().forEach(action)
|
||||
fun reader(path: String) = resolve(path).reader()
|
||||
fun writer(path: String) = resolve(path).writer()
|
||||
|
||||
fun replace(path: String, oldValue: String, newValue: String, oldValueIsRegex: Boolean = false) {
|
||||
// TODO: buffer this somehow
|
||||
val content = resolve(path).readText()
|
||||
|
||||
if (oldValueIsRegex) {
|
||||
content.replace(Regex(oldValue), newValue)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun getXmlEditor(path: String) = DomFileEditor(resolve(path))
|
||||
}
|
||||
|
||||
class DomFileEditor internal constructor(private val domFile: File) : Closeable {
|
||||
val file: Document
|
||||
|
||||
init {
|
||||
val factory = DocumentBuilderFactory.newInstance()
|
||||
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
||||
|
||||
val builder = factory.newDocumentBuilder()
|
||||
|
||||
// this will expectedly throw
|
||||
file = builder.parse(domFile)
|
||||
file.normalize()
|
||||
}
|
||||
|
||||
override fun close() = TransformerFactory.newInstance().newTransformer()
|
||||
.transform(DOMSource(file), StreamResult(domFile.outputStream()))
|
||||
}
|
||||
108
src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt
Normal file
108
src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import org.jf.dexlib2.AccessFlags
|
||||
import org.jf.dexlib2.builder.BuilderInstruction
|
||||
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||
import org.jf.dexlib2.iface.Method
|
||||
import org.jf.dexlib2.iface.reference.MethodReference
|
||||
import org.jf.dexlib2.immutable.ImmutableMethod
|
||||
import org.jf.dexlib2.immutable.ImmutableMethodImplementation
|
||||
import org.jf.dexlib2.util.MethodUtil
|
||||
|
||||
/**
|
||||
* Recursively find a given annotation on a class
|
||||
* @param targetAnnotation The annotation to find
|
||||
* @return The annotation
|
||||
*/
|
||||
fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: Class<T>) =
|
||||
this.findAnnotationRecursively(targetAnnotation, mutableSetOf())
|
||||
|
||||
private 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
|
||||
}
|
||||
|
||||
infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value
|
||||
infix fun Int.or(other: AccessFlags) = this or other.value
|
||||
|
||||
fun MutableMethodImplementation.addInstructions(index: Int, instructions: List<BuilderInstruction>) {
|
||||
for (i in instructions.lastIndex downTo 0) {
|
||||
this.addInstruction(index, instructions[i])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the method.
|
||||
* @param registerCount This parameter allows you to change the register count of the method.
|
||||
* This may be a positive or negative number.
|
||||
* @return The **immutable** cloned method. Call [toMutable] or [cloneMutable] to get a **mutable** copy.
|
||||
*/
|
||||
internal fun Method.clone(
|
||||
registerCount: Int = 0,
|
||||
): ImmutableMethod {
|
||||
val clonedImplementation = implementation?.let {
|
||||
ImmutableMethodImplementation(
|
||||
it.registerCount + registerCount,
|
||||
it.instructions,
|
||||
it.tryBlocks,
|
||||
it.debugItems,
|
||||
)
|
||||
}
|
||||
return ImmutableMethod(
|
||||
returnType,
|
||||
name,
|
||||
parameters,
|
||||
returnType,
|
||||
accessFlags,
|
||||
annotations,
|
||||
hiddenApiRestrictions,
|
||||
clonedImplementation
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the method.
|
||||
* @param registerCount This parameter allows you to change the register count of the method.
|
||||
* This may be a positive or negative number.
|
||||
* @return The **mutable** cloned method. Call [clone] to get an **immutable** copy.
|
||||
*/
|
||||
internal fun Method.cloneMutable(
|
||||
registerCount: Int = 0,
|
||||
) = clone(registerCount).toMutable()
|
||||
|
||||
internal fun Method.softCompareTo(
|
||||
otherMethod: MethodReference
|
||||
): Boolean {
|
||||
if (MethodUtil.isConstructor(this) && !parametersEqual(this.parameterTypes, otherMethod.parameterTypes))
|
||||
return false
|
||||
return this.name == otherMethod.name
|
||||
}
|
||||
|
||||
// FIXME: also check the order of parameters as different order equals different method overload
|
||||
internal fun parametersEqual(
|
||||
parameters1: Iterable<CharSequence>,
|
||||
parameters2: Iterable<CharSequence>
|
||||
): Boolean {
|
||||
return parameters1.count() == parameters2.count() && parameters1.all { parameter ->
|
||||
parameters2.any {
|
||||
it.startsWith(
|
||||
parameter
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.revanced.patcher.patch.annotations
|
||||
|
||||
/**
|
||||
* Annotation to mark a Class as a patch.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
annotation class Patch
|
||||
18
src/main/kotlin/app/revanced/patcher/patch/base/Patch.kt
Normal file
18
src/main/kotlin/app/revanced/patcher/patch/base/Patch.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package app.revanced.patcher.patch.base
|
||||
|
||||
import app.revanced.patcher.data.base.Data
|
||||
import app.revanced.patcher.patch.implementation.BytecodePatch
|
||||
import app.revanced.patcher.patch.implementation.ResourcePatch
|
||||
import app.revanced.patcher.patch.implementation.misc.PatchResult
|
||||
|
||||
|
||||
/**
|
||||
* A ReVanced patch.
|
||||
* Can either be a [ResourcePatch] or a [BytecodePatch].
|
||||
*/
|
||||
abstract class Patch<out T : Data> {
|
||||
/**
|
||||
* The main function of the [Patch] which the patcher will call.
|
||||
*/
|
||||
abstract fun execute(data: @UnsafeVariance T): PatchResult // FIXME: remove the UnsafeVariance annotation
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.revanced.patcher.patch.implementation
|
||||
|
||||
import app.revanced.patcher.data.implementation.BytecodeData
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
|
||||
/**
|
||||
* Bytecode patch for the Patcher.
|
||||
* @param signatures A list of [MethodSignature] this patch relies on.
|
||||
*/
|
||||
abstract class BytecodePatch(
|
||||
val signatures: Iterable<MethodSignature>
|
||||
) : Patch<BytecodeData>()
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.revanced.patcher.patch.implementation
|
||||
|
||||
import app.revanced.patcher.data.implementation.ResourceData
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
|
||||
/**
|
||||
* Resource patch for the Patcher.
|
||||
*/
|
||||
abstract class ResourcePatch : Patch<ResourceData>()
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.revanced.patcher.patch
|
||||
package app.revanced.patcher.patch.implementation.misc
|
||||
|
||||
interface PatchResult {
|
||||
fun error(): PatchResultError? {
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.revanced.patcher.signature.base
|
||||
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
|
||||
/**
|
||||
* A ReVanced signature.
|
||||
* Can be a [MethodSignature].
|
||||
*/
|
||||
interface Signature
|
||||
@@ -0,0 +1,47 @@
|
||||
package app.revanced.patcher.signature.implementation.method
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.data.implementation.MethodNotFoundException
|
||||
import app.revanced.patcher.signature.base.Signature
|
||||
import app.revanced.patcher.signature.implementation.method.resolver.SignatureResolverResult
|
||||
import org.jf.dexlib2.Opcode
|
||||
|
||||
/**
|
||||
* Represents the [MethodSignature] for a method.
|
||||
* @param returnType The return type of the method.
|
||||
* @param accessFlags The access flags of the method.
|
||||
* @param methodParameters The parameters of the method.
|
||||
* @param opcodes The list of opcodes of the method.
|
||||
* @param strings A list of strings which a method contains.
|
||||
* A `null` opcode is equals to an unknown opcode.
|
||||
*/
|
||||
abstract class MethodSignature(
|
||||
internal val returnType: String?,
|
||||
internal val accessFlags: Int?,
|
||||
internal val methodParameters: Iterable<String>?,
|
||||
internal val opcodes: Iterable<Opcode?>?,
|
||||
internal val strings: Iterable<String>? = null
|
||||
) : Signature {
|
||||
/**
|
||||
* The result of the signature
|
||||
*/
|
||||
var result: SignatureResolverResult? = null
|
||||
get() {
|
||||
return field ?: throw MethodNotFoundException(
|
||||
"Could not resolve required signature ${
|
||||
(this::class.annotations.find { it is Name }?.let {
|
||||
(it as Name).name
|
||||
})
|
||||
}"
|
||||
)
|
||||
}
|
||||
val resolved: Boolean
|
||||
get() {
|
||||
var resolved = false
|
||||
try {
|
||||
resolved = result != null
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.revanced.patcher.signature.implementation.method.annotation
|
||||
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
|
||||
/**
|
||||
* Annotations for a method which matches to a [MethodSignature].
|
||||
* @param definingClass The defining class name of the method.
|
||||
* @param name A suggestive name for the method which the [MethodSignature] was created for.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class MatchingMethod(
|
||||
val definingClass: String = "L<unspecified-class>",
|
||||
val name: String = "<unspecified-method>"
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotations to scan a pattern [MethodSignature] with fuzzy algorithm.
|
||||
* @param threshold if [threshold] or more of the opcodes do not match, skip.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class FuzzyPatternScanMethod(
|
||||
val threshold: Int = 1
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotations to scan a pattern [MethodSignature] directly.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class DirectPatternScanMethod
|
||||
@@ -0,0 +1,167 @@
|
||||
package app.revanced.patcher.signature.implementation.method.resolver
|
||||
|
||||
import app.revanced.patcher.data.PatcherData
|
||||
import app.revanced.patcher.data.implementation.proxy
|
||||
import app.revanced.patcher.extensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.extensions.parametersEqual
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
import app.revanced.patcher.signature.implementation.method.annotation.FuzzyPatternScanMethod
|
||||
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.formats.Instruction21c
|
||||
import org.jf.dexlib2.iface.reference.StringReference
|
||||
|
||||
internal class MethodSignatureResolver(
|
||||
private val classes: List<ClassDef>,
|
||||
private val methodSignatures: Iterable<MethodSignature>
|
||||
) {
|
||||
fun resolve(patcherData: PatcherData) {
|
||||
for (signature in methodSignatures) {
|
||||
for (classDef in classes) {
|
||||
for (method in classDef.methods) {
|
||||
val patternScanData = compareSignatureToMethod(signature, method) ?: continue
|
||||
|
||||
// create class proxy, in case a patch needs mutability
|
||||
val classProxy = patcherData.bytecodeData.proxy(classDef)
|
||||
signature.result = SignatureResolverResult(
|
||||
classProxy,
|
||||
patternScanData,
|
||||
method,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// These functions do not require the constructor values, so they can be static.
|
||||
companion object {
|
||||
fun resolveFromProxy(
|
||||
classProxy: app.revanced.patcher.util.proxy.ClassProxy,
|
||||
signature: MethodSignature
|
||||
): SignatureResolverResult? {
|
||||
for (method in classProxy.immutableClass.methods) {
|
||||
val result = compareSignatureToMethod(signature, method) ?: continue
|
||||
return SignatureResolverResult(
|
||||
classProxy,
|
||||
result,
|
||||
method,
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun compareSignatureToMethod(
|
||||
signature: MethodSignature,
|
||||
method: Method
|
||||
): PatternScanResult? {
|
||||
signature.returnType?.let {
|
||||
if (!method.returnType.startsWith(signature.returnType)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
signature.accessFlags?.let {
|
||||
if (signature.accessFlags != method.accessFlags) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
signature.methodParameters?.let {
|
||||
if (!parametersEqual(signature.methodParameters, method.parameterTypes)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
signature.strings?.let { strings ->
|
||||
method.implementation ?: return null
|
||||
|
||||
method.implementation!!.instructions.let { instructions ->
|
||||
val stringsList = strings.toMutableList()
|
||||
|
||||
for (instruction in instructions) {
|
||||
if (instruction.opcode != Opcode.CONST_STRING) continue
|
||||
|
||||
val string = ((instruction as Instruction21c).reference as StringReference).string
|
||||
val i = stringsList.indexOfFirst { it == string }
|
||||
if (i != -1) stringsList.removeAt(i)
|
||||
}
|
||||
|
||||
if (stringsList.isNotEmpty()) return null
|
||||
}
|
||||
}
|
||||
|
||||
return if (signature.opcodes == null) {
|
||||
PatternScanResult(0, 0)
|
||||
} else {
|
||||
method.implementation?.instructions?.let {
|
||||
compareOpcodes(signature, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun compareOpcodes(
|
||||
signature: MethodSignature,
|
||||
instructions: Iterable<Instruction>
|
||||
): PatternScanResult? {
|
||||
val count = instructions.count()
|
||||
val pattern = signature.opcodes!!
|
||||
val size = pattern.count()
|
||||
|
||||
val threshold =
|
||||
signature::class.java.findAnnotationRecursively(FuzzyPatternScanMethod::class.java)?.threshold
|
||||
?: 0
|
||||
|
||||
for (instructionIndex in 0 until count) {
|
||||
var patternIndex = 0
|
||||
var currentThreshold = threshold
|
||||
while (instructionIndex + patternIndex < count) {
|
||||
val originalOpcode = instructions.elementAt(instructionIndex + patternIndex).opcode
|
||||
val patternOpcode = pattern.elementAt(patternIndex)
|
||||
if (
|
||||
patternOpcode != null && // unknown opcode
|
||||
originalOpcode != patternOpcode &&
|
||||
currentThreshold-- == 0
|
||||
) break
|
||||
if (++patternIndex < size) continue
|
||||
patternIndex-- // fix pattern offset
|
||||
|
||||
val result = PatternScanResult(instructionIndex, instructionIndex + patternIndex)
|
||||
|
||||
result.warnings = generateWarnings(signature, instructions, result)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun generateWarnings(
|
||||
signature: MethodSignature,
|
||||
instructions: Iterable<Instruction>,
|
||||
scanResult: PatternScanResult,
|
||||
) = buildList {
|
||||
val pattern = signature.opcodes!!
|
||||
for ((patternIndex, instructionIndex) in (scanResult.startIndex until scanResult.endIndex).withIndex()) {
|
||||
val correctOpcode = instructions.elementAt(instructionIndex).opcode
|
||||
val patternOpcode = pattern.elementAt(patternIndex)
|
||||
if (
|
||||
patternOpcode != null && // unknown opcode
|
||||
correctOpcode != patternOpcode
|
||||
) {
|
||||
this.add(
|
||||
PatternScanResult.Warning(
|
||||
correctOpcode, patternOpcode,
|
||||
instructionIndex, patternIndex,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun ClassDef.component1() = this
|
||||
private operator fun ClassDef.component2() = this.methods
|
||||
@@ -0,0 +1,75 @@
|
||||
package app.revanced.patcher.signature.implementation.method.resolver
|
||||
|
||||
import app.revanced.patcher.extensions.softCompareTo
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
import org.jf.dexlib2.Opcode
|
||||
import org.jf.dexlib2.iface.Method
|
||||
|
||||
/**
|
||||
* Represents the result of a [MethodSignatureResolver].
|
||||
* @param definingClassProxy The [ClassProxy] that the matching method was found in.
|
||||
* @param resolvedMethod The actual matching method.
|
||||
* @param scanResult Opcodes pattern scan result.
|
||||
*/
|
||||
data class SignatureResolverResult(
|
||||
val definingClassProxy: ClassProxy,
|
||||
val scanResult: PatternScanResult,
|
||||
private val resolvedMethod: Method,
|
||||
) {
|
||||
/**
|
||||
* Returns the **mutable** method by the [resolvedMethod] from the [definingClassProxy].
|
||||
*
|
||||
* Please note, this method allocates a [ClassProxy].
|
||||
* Use [immutableMethod] where possible.
|
||||
*/
|
||||
val method
|
||||
get() = definingClassProxy.resolve().methods.first {
|
||||
it.softCompareTo(resolvedMethod)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the **immutable** method by the [resolvedMethod] from the [definingClassProxy].
|
||||
*
|
||||
* If you need to modify the method, use [method] instead.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
val immutableMethod: Method
|
||||
get() = definingClassProxy.immutableClass.methods.first {
|
||||
it.softCompareTo(resolvedMethod)
|
||||
}
|
||||
|
||||
fun findParentMethod(signature: MethodSignature): SignatureResolverResult? {
|
||||
return MethodSignatureResolver.resolveFromProxy(definingClassProxy, signature)
|
||||
}
|
||||
}
|
||||
|
||||
data class PatternScanResult(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int
|
||||
) {
|
||||
/**
|
||||
* A list of warnings the resolver found.
|
||||
*
|
||||
* This list will be allocated when the signature has been found.
|
||||
* Meaning, if the signature was not found,
|
||||
* or the signature was not yet resolved,
|
||||
* the list will be null.
|
||||
*/
|
||||
var warnings: List<Warning>? = null
|
||||
|
||||
/**
|
||||
* Represents a resolver warning.
|
||||
* @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,
|
||||
)
|
||||
}
|
||||
|
||||
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,45 @@
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import org.jf.dexlib2.iface.ClassDef
|
||||
|
||||
class ProxyBackedClassList(internal val internalClasses: MutableList<ClassDef>) : List<ClassDef> {
|
||||
internal val proxies = mutableListOf<app.revanced.patcher.util.proxy.ClassProxy>()
|
||||
|
||||
fun add(classDef: ClassDef) {
|
||||
internalClasses.add(classDef)
|
||||
}
|
||||
|
||||
fun add(classProxy: app.revanced.patcher.util.proxy.ClassProxy) {
|
||||
proxies.add(classProxy)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all resolved classes into [internalClasses] and clean the [proxies] list.
|
||||
*/
|
||||
fun applyProxies() {
|
||||
// FIXME: check if this could cause issues when multiple patches use the same proxy
|
||||
proxies.removeIf { proxy ->
|
||||
// if the proxy is unused, keep it in the list
|
||||
if (!proxy.proxyUsed) return@removeIf false
|
||||
|
||||
// if it has been used, replace the internal class which it proxied
|
||||
val index = internalClasses.indexOfFirst { it.type == proxy.immutableClass.type }
|
||||
internalClasses[index] = proxy.mutatedClass
|
||||
|
||||
// return true to remove it from the proxies list
|
||||
return@removeIf true
|
||||
}
|
||||
}
|
||||
|
||||
override val size get() = internalClasses.size
|
||||
override fun contains(element: ClassDef) = internalClasses.contains(element)
|
||||
override fun containsAll(elements: Collection<ClassDef>) = internalClasses.containsAll(elements)
|
||||
override fun get(index: Int) = internalClasses[index]
|
||||
override fun indexOf(element: ClassDef) = internalClasses.indexOf(element)
|
||||
override fun isEmpty() = internalClasses.isEmpty()
|
||||
override fun iterator() = internalClasses.iterator()
|
||||
override fun lastIndexOf(element: ClassDef) = internalClasses.lastIndexOf(element)
|
||||
override fun listIterator() = internalClasses.listIterator()
|
||||
override fun listIterator(index: Int) = internalClasses.listIterator(index)
|
||||
override fun subList(fromIndex: Int, toIndex: Int) = internalClasses.subList(fromIndex, toIndex)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package app.revanced.patcher.util.method
|
||||
|
||||
import app.revanced.patcher.data.implementation.BytecodeData
|
||||
import app.revanced.patcher.data.implementation.MethodNotFoundException
|
||||
import app.revanced.patcher.extensions.softCompareTo
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import org.jf.dexlib2.Format
|
||||
import org.jf.dexlib2.iface.Method
|
||||
import org.jf.dexlib2.iface.instruction.formats.Instruction35c
|
||||
import org.jf.dexlib2.iface.reference.MethodReference
|
||||
import org.jf.dexlib2.util.Preconditions
|
||||
|
||||
/**
|
||||
* Find a method from another method via instruction offsets.
|
||||
* @param bytecodeData The bytecodeData to use when resolving the next method reference.
|
||||
* @param currentMethod The method to start from.
|
||||
*/
|
||||
class MethodWalker internal constructor(
|
||||
private val bytecodeData: BytecodeData,
|
||||
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.
|
||||
*/
|
||||
fun getMethod(): Method {
|
||||
return currentMethod
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk to a method defined at the offset in the instruction list of the current method.
|
||||
* @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.
|
||||
* The current method will be mutable.
|
||||
*/
|
||||
fun walk(offset: Int, walkMutable: Boolean = false): MethodWalker {
|
||||
currentMethod.implementation?.instructions?.let { instructions ->
|
||||
val instruction = instructions.elementAt(offset)
|
||||
|
||||
Preconditions.checkFormat(instruction.opcode, Format.Format35c)
|
||||
|
||||
val newMethod = (instruction as Instruction35c).reference as MethodReference
|
||||
val proxy = bytecodeData.findClass(newMethod.definingClass)!!
|
||||
|
||||
val methods = if (walkMutable) proxy.resolve().methods else proxy.immutableClass.methods
|
||||
currentMethod = methods.first { it ->
|
||||
return@first it.softCompareTo(newMethod)
|
||||
}
|
||||
return this
|
||||
}
|
||||
throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package app.revanced.patcher.util.patch
|
||||
|
||||
import app.revanced.patcher.patch.base.Patch
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarFile
|
||||
|
||||
object PatchLoader {
|
||||
/**
|
||||
* This method loads patches from a given jar file containing [Patch]es
|
||||
* @return the loaded patches represented as a list of [Patch] classes
|
||||
*/
|
||||
fun loadFromFile(patchesJar: File) = buildList {
|
||||
val jarFile = JarFile(patchesJar)
|
||||
val classLoader = URLClassLoader(arrayOf(patchesJar.toURI().toURL()))
|
||||
|
||||
val entries = jarFile.entries()
|
||||
while (entries.hasMoreElements()) {
|
||||
val entry = entries.nextElement()
|
||||
if (!entry.name.endsWith(".class") || entry.name.contains("$")) continue
|
||||
|
||||
val clazz = classLoader.loadClass(entry.realName.replace('/', '.').replace(".class", ""))
|
||||
|
||||
if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) continue
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val patch = clazz as Class<Patch<*>>
|
||||
|
||||
// TODO: include declared classes from patch
|
||||
|
||||
this.add(patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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(
|
||||
val immutableClass: ClassDef,
|
||||
) {
|
||||
internal var proxyUsed = false
|
||||
internal lateinit var mutatedClass: MutableClass
|
||||
|
||||
init {
|
||||
// in the instance, that a [MutableClass] is being proxied,
|
||||
// do not create an additional clone and reuse the [MutableClass] instance
|
||||
if (immutableClass is MutableClass) {
|
||||
mutatedClass = immutableClass
|
||||
proxyUsed = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates and returns a mutable clone of the original class.
|
||||
* A patch should always use the original immutable class reference
|
||||
* to avoid unnecessary allocations for the mutable class.
|
||||
* @return A mutable clone of the original class.
|
||||
*/
|
||||
fun resolve(): MutableClass {
|
||||
if (!proxyUsed) {
|
||||
proxyUsed = true
|
||||
mutatedClass = MutableClass(immutableClass)
|
||||
}
|
||||
return mutatedClass
|
||||
}
|
||||
}
|
||||
@@ -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,64 @@
|
||||
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 }
|
||||
|
||||
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,65 @@
|
||||
package app.revanced.patcher.util.smali
|
||||
|
||||
import org.antlr.runtime.CommonTokenStream
|
||||
import org.antlr.runtime.TokenSource
|
||||
import org.antlr.runtime.tree.CommonTreeNodeStream
|
||||
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 public Linlinecompiler;
|
||||
.super Ljava/lang/Object;
|
||||
.method public static compiler(%s)V
|
||||
.registers %d
|
||||
%s
|
||||
.end method
|
||||
"""
|
||||
|
||||
class InlineSmaliCompiler {
|
||||
companion object {
|
||||
/**
|
||||
* Compiles a string of Smali code to a list of instructions.
|
||||
* p0, p1 etc. will only work correctly if the parameters and registers are passed.
|
||||
* Do not cross the boundaries of the control flow (if-nez insn, etc),
|
||||
* as that will result in exceptions since the labels cannot be calculated.
|
||||
* Do not create dummy labels to fix the issue, since the code addresses will
|
||||
* be messed up and results in broken Dalvik bytecode.
|
||||
* FIXME: Fix the above issue. When this is fixed, add the proper conversions in [InstructionConverter].
|
||||
*/
|
||||
fun compileMethodInstructions(
|
||||
instructions: String,
|
||||
parameters: String,
|
||||
registers: Int
|
||||
): List<BuilderInstruction> {
|
||||
val input = METHOD_TEMPLATE.format(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.toBuilderInstruction() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toInstructions(parameters: String = "", registers: Int = 1) =
|
||||
InlineSmaliCompiler.compileMethodInstructions(this, parameters, registers)
|
||||
|
||||
fun String.toInstruction(parameters: String = "", registers: Int = 1) =
|
||||
this.toInstructions(parameters, registers).first()
|
||||
@@ -0,0 +1,261 @@
|
||||
package app.revanced.patcher.util.smali
|
||||
|
||||
import org.jf.dexlib2.Format
|
||||
import org.jf.dexlib2.builder.instruction.*
|
||||
import org.jf.dexlib2.iface.instruction.Instruction
|
||||
import org.jf.dexlib2.iface.instruction.formats.*
|
||||
import org.jf.util.ExceptionWithContext
|
||||
|
||||
fun Instruction.toBuilderInstruction() =
|
||||
when (this.opcode.format) {
|
||||
Format.Format10x -> InstructionConverter.newBuilderInstruction10x(this as Instruction10x)
|
||||
Format.Format11n -> InstructionConverter.newBuilderInstruction11n(this as Instruction11n)
|
||||
Format.Format11x -> InstructionConverter.newBuilderInstruction11x(this as Instruction11x)
|
||||
Format.Format12x -> InstructionConverter.newBuilderInstruction12x(this as Instruction12x)
|
||||
Format.Format20bc -> InstructionConverter.newBuilderInstruction20bc(this as Instruction20bc)
|
||||
Format.Format21c -> InstructionConverter.newBuilderInstruction21c(this as Instruction21c)
|
||||
Format.Format21ih -> InstructionConverter.newBuilderInstruction21ih(this as Instruction21ih)
|
||||
Format.Format21lh -> InstructionConverter.newBuilderInstruction21lh(this as Instruction21lh)
|
||||
Format.Format21s -> InstructionConverter.newBuilderInstruction21s(this as Instruction21s)
|
||||
Format.Format22b -> InstructionConverter.newBuilderInstruction22b(this as Instruction22b)
|
||||
Format.Format22c -> InstructionConverter.newBuilderInstruction22c(this as Instruction22c)
|
||||
Format.Format22cs -> InstructionConverter.newBuilderInstruction22cs(this as Instruction22cs)
|
||||
Format.Format22s -> InstructionConverter.newBuilderInstruction22s(this as Instruction22s)
|
||||
Format.Format22x -> InstructionConverter.newBuilderInstruction22x(this as Instruction22x)
|
||||
Format.Format23x -> InstructionConverter.newBuilderInstruction23x(this as Instruction23x)
|
||||
Format.Format31c -> InstructionConverter.newBuilderInstruction31c(this as Instruction31c)
|
||||
Format.Format31i -> InstructionConverter.newBuilderInstruction31i(this as Instruction31i)
|
||||
Format.Format32x -> InstructionConverter.newBuilderInstruction32x(this as Instruction32x)
|
||||
Format.Format35c -> InstructionConverter.newBuilderInstruction35c(this as Instruction35c)
|
||||
Format.Format35mi -> InstructionConverter.newBuilderInstruction35mi(this as Instruction35mi)
|
||||
Format.Format35ms -> InstructionConverter.newBuilderInstruction35ms(this as Instruction35ms)
|
||||
Format.Format3rc -> InstructionConverter.newBuilderInstruction3rc(this as Instruction3rc)
|
||||
Format.Format3rmi -> InstructionConverter.newBuilderInstruction3rmi(this as Instruction3rmi)
|
||||
Format.Format3rms -> InstructionConverter.newBuilderInstruction3rms(this as Instruction3rms)
|
||||
Format.Format51l -> InstructionConverter.newBuilderInstruction51l(this as Instruction51l)
|
||||
else -> throw ExceptionWithContext("Instruction format %s not supported", this.opcode.format)
|
||||
}
|
||||
|
||||
internal class InstructionConverter {
|
||||
companion object {
|
||||
internal fun newBuilderInstruction10x(instruction: Instruction10x): BuilderInstruction10x {
|
||||
return BuilderInstruction10x(
|
||||
instruction.opcode
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction11n(instruction: Instruction11n): BuilderInstruction11n {
|
||||
return BuilderInstruction11n(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.narrowLiteral
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction11x(instruction: Instruction11x): BuilderInstruction11x {
|
||||
return BuilderInstruction11x(
|
||||
instruction.opcode,
|
||||
instruction.registerA
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction12x(instruction: Instruction12x): BuilderInstruction12x {
|
||||
return BuilderInstruction12x(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.registerB
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction20bc(instruction: Instruction20bc): BuilderInstruction20bc {
|
||||
return BuilderInstruction20bc(
|
||||
instruction.opcode,
|
||||
instruction.verificationError,
|
||||
instruction.reference
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction21c(instruction: Instruction21c): BuilderInstruction21c {
|
||||
return BuilderInstruction21c(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.reference
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction21ih(instruction: Instruction21ih): BuilderInstruction21ih {
|
||||
return BuilderInstruction21ih(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.narrowLiteral
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction21lh(instruction: Instruction21lh): BuilderInstruction21lh {
|
||||
return BuilderInstruction21lh(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.wideLiteral
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction21s(instruction: Instruction21s): BuilderInstruction21s {
|
||||
return BuilderInstruction21s(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.narrowLiteral
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction22b(instruction: Instruction22b): BuilderInstruction22b {
|
||||
return BuilderInstruction22b(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.registerB,
|
||||
instruction.narrowLiteral
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction22c(instruction: Instruction22c): BuilderInstruction22c {
|
||||
return BuilderInstruction22c(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.registerB,
|
||||
instruction.reference
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction22cs(instruction: Instruction22cs): BuilderInstruction22cs {
|
||||
return BuilderInstruction22cs(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.registerB,
|
||||
instruction.fieldOffset
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction22s(instruction: Instruction22s): BuilderInstruction22s {
|
||||
return BuilderInstruction22s(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.registerB,
|
||||
instruction.narrowLiteral
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction22x(instruction: Instruction22x): BuilderInstruction22x {
|
||||
return BuilderInstruction22x(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.registerB
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction23x(instruction: Instruction23x): BuilderInstruction23x {
|
||||
return BuilderInstruction23x(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.registerB,
|
||||
instruction.registerC
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction31c(instruction: Instruction31c): BuilderInstruction31c {
|
||||
return BuilderInstruction31c(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.reference
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction31i(instruction: Instruction31i): BuilderInstruction31i {
|
||||
return BuilderInstruction31i(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.narrowLiteral
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction32x(instruction: Instruction32x): BuilderInstruction32x {
|
||||
return BuilderInstruction32x(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.registerB
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction35c(instruction: Instruction35c): BuilderInstruction35c {
|
||||
return BuilderInstruction35c(
|
||||
instruction.opcode,
|
||||
instruction.registerCount,
|
||||
instruction.registerC,
|
||||
instruction.registerD,
|
||||
instruction.registerE,
|
||||
instruction.registerF,
|
||||
instruction.registerG,
|
||||
instruction.reference
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction35mi(instruction: Instruction35mi): BuilderInstruction35mi {
|
||||
return BuilderInstruction35mi(
|
||||
instruction.opcode,
|
||||
instruction.registerCount,
|
||||
instruction.registerC,
|
||||
instruction.registerD,
|
||||
instruction.registerE,
|
||||
instruction.registerF,
|
||||
instruction.registerG,
|
||||
instruction.inlineIndex
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction35ms(instruction: Instruction35ms): BuilderInstruction35ms {
|
||||
return BuilderInstruction35ms(
|
||||
instruction.opcode,
|
||||
instruction.registerCount,
|
||||
instruction.registerC,
|
||||
instruction.registerD,
|
||||
instruction.registerE,
|
||||
instruction.registerF,
|
||||
instruction.registerG,
|
||||
instruction.vtableIndex
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction3rc(instruction: Instruction3rc): BuilderInstruction3rc {
|
||||
return BuilderInstruction3rc(
|
||||
instruction.opcode,
|
||||
instruction.startRegister,
|
||||
instruction.registerCount,
|
||||
instruction.reference
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction3rmi(instruction: Instruction3rmi): BuilderInstruction3rmi {
|
||||
return BuilderInstruction3rmi(
|
||||
instruction.opcode,
|
||||
instruction.startRegister,
|
||||
instruction.registerCount,
|
||||
instruction.inlineIndex
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction3rms(instruction: Instruction3rms): BuilderInstruction3rms {
|
||||
return BuilderInstruction3rms(
|
||||
instruction.opcode,
|
||||
instruction.startRegister,
|
||||
instruction.registerCount,
|
||||
instruction.vtableIndex
|
||||
)
|
||||
}
|
||||
|
||||
internal fun newBuilderInstruction51l(instruction: Instruction51l): BuilderInstruction51l {
|
||||
return BuilderInstruction51l(
|
||||
instruction.opcode,
|
||||
instruction.registerA,
|
||||
instruction.wideLiteral
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
46
src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
46
src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class PatcherTest {
|
||||
@Test
|
||||
fun testPatcher() {
|
||||
return // FIXME: create a proper resource to pass this test
|
||||
/**
|
||||
val patcher = Patcher(
|
||||
File(PatcherTest::class.java.getResource("/example.apk")!!.toURI()),
|
||||
"exampleCacheDirectory",
|
||||
patchResources = true
|
||||
)
|
||||
|
||||
patcher.addPatches(listOf(ExampleBytecodePatch(), ExampleResourcePatch()))
|
||||
|
||||
for (signature in patcher.resolveSignatures()) {
|
||||
if (!signature.resolved) {
|
||||
throw Exception("Signature ${signature.metadata.name} was not resolved!")
|
||||
}
|
||||
val patternScanMethod = signature.metadata.patternScanMethod
|
||||
if (patternScanMethod is PatternScanMethod.Fuzzy) {
|
||||
val warnings = patternScanMethod.warnings
|
||||
if (warnings != null) {
|
||||
println("Signature ${signature.metadata.name} had ${warnings.size} warnings!")
|
||||
for (warning in warnings) {
|
||||
println(warning.toString())
|
||||
}
|
||||
} else {
|
||||
println("Signature ${signature.metadata.name} used the fuzzy resolver, but the warnings list is null!")
|
||||
}
|
||||
}
|
||||
}
|
||||
for ((metadata, result) in patcher.applyPatches()) {
|
||||
if (result.isFailure) {
|
||||
throw Exception("Patch ${metadata.shortName} failed", result.exceptionOrNull()!!)
|
||||
} else {
|
||||
println("Patch ${metadata.shortName} applied successfully!")
|
||||
}
|
||||
}
|
||||
val out = patcher.save()
|
||||
assertTrue(out.isNotEmpty(), "Expected the output of Patcher#save() to not be empty.")
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.revanced.patcher.usage.bytecode.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)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class ExampleBytecodeCompatibility
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package app.revanced.patcher.usage.bytecode.patch
|
||||
|
||||
import app.revanced.patcher.annotation.Description
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.data.implementation.BytecodeData
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.extensions.or
|
||||
import app.revanced.patcher.patch.annotations.Patch
|
||||
import app.revanced.patcher.patch.implementation.BytecodePatch
|
||||
import app.revanced.patcher.patch.implementation.misc.PatchResult
|
||||
import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess
|
||||
import app.revanced.patcher.usage.bytecode.signatures.ExampleSignature
|
||||
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patcher.util.smali.toInstruction
|
||||
import app.revanced.patcher.util.smali.toInstructions
|
||||
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
|
||||
|
||||
@Patch
|
||||
@Name("example-bytecode-patch")
|
||||
@Description("Example demonstration of a bytecode patch.")
|
||||
@ExampleResourceCompatibility
|
||||
@Version("0.0.1")
|
||||
class ExampleBytecodePatch : BytecodePatch(
|
||||
|
||||
listOf(
|
||||
ExampleSignature
|
||||
)
|
||||
) {
|
||||
// This function will be executed by the patcher.
|
||||
// You can treat it as a constructor
|
||||
override fun execute(data: BytecodeData): PatchResult {
|
||||
// Get the resolved method for the signature from the resolver cache
|
||||
val result = signatures.first().result!!
|
||||
|
||||
// Get the implementation for the resolved method
|
||||
val implementation = result.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.startIndex
|
||||
|
||||
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
|
||||
|
||||
// Get the class in which the method matching our signature is defined in.
|
||||
val mainClass = data.findClass {
|
||||
it.type == result.definingClassProxy.immutableClass.type
|
||||
}!!.resolve()
|
||||
|
||||
// Add a new method returning a string
|
||||
mainClass.methods.add(
|
||||
ImmutableMethod(
|
||||
result.definingClassProxy.immutableClass.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
|
||||
implementation.replaceInstruction(
|
||||
0,
|
||||
"sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;".toInstruction()
|
||||
)
|
||||
|
||||
// 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.
|
||||
val instructions = """
|
||||
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
|
||||
move-result-object v1
|
||||
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||
""".trimIndent().toInstructions()
|
||||
implementation.addInstructions(startIndex + 2, instructions)
|
||||
|
||||
// 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.revanced.patcher.usage.bytecode.signatures
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.extensions.or
|
||||
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||
import app.revanced.patcher.signature.implementation.method.annotation.FuzzyPatternScanMethod
|
||||
import app.revanced.patcher.signature.implementation.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.usage.bytecode.annotation.ExampleBytecodeCompatibility
|
||||
import org.jf.dexlib2.AccessFlags
|
||||
import org.jf.dexlib2.Opcode
|
||||
|
||||
@Name("example-signature")
|
||||
@MatchingMethod(
|
||||
"LexampleClass;",
|
||||
"exampleMehod"
|
||||
)
|
||||
@FuzzyPatternScanMethod(2)
|
||||
@ExampleBytecodeCompatibility
|
||||
@Version("0.0.1")
|
||||
object ExampleSignature : MethodSignature(
|
||||
"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,14 @@
|
||||
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)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class ExampleResourceCompatibility
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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.implementation.ResourceData
|
||||
import app.revanced.patcher.patch.annotations.Patch
|
||||
import app.revanced.patcher.patch.implementation.ResourcePatch
|
||||
import app.revanced.patcher.patch.implementation.misc.PatchResult
|
||||
import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess
|
||||
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(data: ResourceData): PatchResult {
|
||||
val editor = data.getXmlEditor("AndroidManifest.xml")
|
||||
|
||||
// regular DomFileEditor
|
||||
val element = editor
|
||||
.file
|
||||
.getElementsByTagName("application")
|
||||
.item(0) as Element
|
||||
element
|
||||
.setAttribute(
|
||||
"exampleAttribute",
|
||||
"exampleValue"
|
||||
)
|
||||
|
||||
// close the editor to write changes
|
||||
editor.close()
|
||||
|
||||
// iterate through all available resources
|
||||
data.forEach {
|
||||
if (it.extension.lowercase() != "xml") return@forEach
|
||||
|
||||
data.replace(
|
||||
it.path,
|
||||
"\\ddip", // regex supported
|
||||
"0dip",
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package net.revanced.patcher
|
||||
|
||||
import net.revanced.patcher.patch.Patch
|
||||
import net.revanced.patcher.patch.PatchResultSuccess
|
||||
import net.revanced.patcher.signature.Signature
|
||||
import net.revanced.patcher.util.ExtraTypes
|
||||
import net.revanced.patcher.util.TestUtil
|
||||
import net.revanced.patcher.writer.ASMWriter.insertAt
|
||||
import net.revanced.patcher.writer.ASMWriter.setAt
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import org.objectweb.asm.Type
|
||||
import org.objectweb.asm.tree.*
|
||||
import java.io.PrintStream
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class PatcherTest {
|
||||
companion object {
|
||||
val testSigs: Array<Signature> = arrayOf(
|
||||
// Java:
|
||||
// public static void main(String[] args) {
|
||||
// System.out.println("Hello, world!");
|
||||
// }
|
||||
// Bytecode:
|
||||
// public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream
|
||||
// ldc "Hello, world!" (java.lang.String)
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// return
|
||||
// }
|
||||
Signature(
|
||||
"mainMethod",
|
||||
Type.VOID_TYPE,
|
||||
ACC_PUBLIC or ACC_STATIC,
|
||||
arrayOf(ExtraTypes.ArrayAny),
|
||||
arrayOf(
|
||||
LDC,
|
||||
INVOKEVIRTUAL
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPatcher() {
|
||||
val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!!
|
||||
val patcher = Patcher(testData, testSigs)
|
||||
|
||||
patcher.addPatches(
|
||||
Patch ("TestPatch") {
|
||||
// Get the method from the resolver cache
|
||||
val mainMethod = patcher.cache.methods["mainMethod"]
|
||||
// Get the instruction list
|
||||
val instructions = mainMethod.method.instructions!!
|
||||
|
||||
// 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 LDC instruction.
|
||||
val startIndex = mainMethod.scanData.startIndex
|
||||
TestUtil.assertNodeEqual(LdcInsnNode("Hello, world!"), instructions[startIndex]!!)
|
||||
// Create a new LDC node and replace the LDC instruction.
|
||||
val stringNode = LdcInsnNode("Hello, ReVanced! Editing bytecode.")
|
||||
instructions.setAt(startIndex, stringNode)
|
||||
|
||||
// Now lets print our string twice!
|
||||
// Insert our instructions after the second instruction by our pattern.
|
||||
// This will place our instructions after the original INVOKEVIRTUAL call.
|
||||
// You could also copy the instructions from the list and then modify the LDC instruction again,
|
||||
// but this is to show a more advanced example of writing bytecode using the patcher and ASM.
|
||||
instructions.insertAt(
|
||||
startIndex + 1,
|
||||
FieldInsnNode(
|
||||
GETSTATIC,
|
||||
Type.getInternalName(System::class.java), // "java/io/System"
|
||||
"out",
|
||||
Type.getInternalName(PrintStream::class.java) // "java.io.PrintStream"
|
||||
),
|
||||
LdcInsnNode("Hello, ReVanced! Adding bytecode."),
|
||||
MethodInsnNode(
|
||||
INVOKEVIRTUAL,
|
||||
Type.getInternalName(PrintStream::class.java), // "java/io/PrintStream"
|
||||
"println",
|
||||
Type.getMethodDescriptor(
|
||||
Type.VOID_TYPE,
|
||||
Type.getType(String::class.java)
|
||||
) // "(Ljava/lang/String;)V"
|
||||
)
|
||||
)
|
||||
|
||||
// Our code now looks like this:
|
||||
// public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream
|
||||
// ldc "Hello, ReVanced! Editing bytecode." (java.lang.String) // We overwrote this instruction.
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// getstatic java/lang/System.out:java.io.PrintStream // This instruction and the 2 instructions below are written manually.
|
||||
// ldc "Hello, ReVanced! Adding bytecode." (java.lang.String)
|
||||
// invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
|
||||
// return
|
||||
// }
|
||||
|
||||
// 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.
|
||||
PatchResultSuccess()
|
||||
}
|
||||
)
|
||||
|
||||
// Apply all patches loaded in the patcher
|
||||
val result = patcher.applyPatches()
|
||||
// You can check if an error occurred
|
||||
for ((s, r) in result) {
|
||||
if (r.isFailure) {
|
||||
throw Exception("Patch $s failed", r.exceptionOrNull()!!)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Doesn't work, needs to be fixed.
|
||||
//val out = ByteArrayOutputStream()
|
||||
//patcher.saveTo(out)
|
||||
//assertTrue(
|
||||
// // 8 is a random value, it's just weird if it's any lower than that
|
||||
// out.size() > 8,
|
||||
// "Output must be at least 8 bytes"
|
||||
//)
|
||||
//
|
||||
//out.close()
|
||||
testData.close()
|
||||
}
|
||||
|
||||
// TODO Doesn't work, needs to be fixed.
|
||||
//@Test
|
||||
//fun `test patcher with no changes`() {
|
||||
// val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!!
|
||||
// val available = testData.available()
|
||||
// val patcher = Patcher(testData, testSigs)
|
||||
//
|
||||
// val out = ByteArrayOutputStream()
|
||||
// patcher.saveTo(out)
|
||||
// assertEquals(available, out.size())
|
||||
//
|
||||
// out.close()
|
||||
// testData.close()
|
||||
//}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package net.revanced.patcher
|
||||
|
||||
import kotlin.test.Test
|
||||
|
||||
internal class ReaderTest {
|
||||
@Test
|
||||
fun `read jar containing multiple classes`() {
|
||||
val testData = PatcherTest::class.java.getResourceAsStream("/test2.jar")!!
|
||||
Patcher(testData, PatcherTest.testSigs) // reusing test sigs from PatcherTest
|
||||
testData.close()
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package net.revanced.patcher.util
|
||||
|
||||
import org.objectweb.asm.tree.AbstractInsnNode
|
||||
import org.objectweb.asm.tree.FieldInsnNode
|
||||
import org.objectweb.asm.tree.LdcInsnNode
|
||||
import kotlin.test.fail
|
||||
|
||||
object TestUtil {
|
||||
fun <T: AbstractInsnNode> assertNodeEqual(expected: T, actual: T) {
|
||||
val a = expected.nodeString()
|
||||
val b = actual.nodeString()
|
||||
if (a != b) {
|
||||
fail("expected: $a,\nactual: $b\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun AbstractInsnNode.nodeString(): String {
|
||||
val sb = NodeStringBuilder()
|
||||
when (this) {
|
||||
// TODO: Add more types
|
||||
is LdcInsnNode -> sb
|
||||
.addType("cst", cst)
|
||||
is FieldInsnNode -> sb
|
||||
.addType("owner", owner)
|
||||
.addType("name", name)
|
||||
.addType("desc", desc)
|
||||
}
|
||||
return "(${this::class.simpleName}): (type = $type, opcode = $opcode, $sb)"
|
||||
}
|
||||
}
|
||||
|
||||
private class NodeStringBuilder {
|
||||
private val sb = StringBuilder()
|
||||
|
||||
fun addType(name: String, value: Any): NodeStringBuilder {
|
||||
sb.append("$name = \"$value\", ")
|
||||
return this
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val s = sb.toString()
|
||||
return s.substring(0 until s.length - 2) // remove the last ", "
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user