├── .editorconfig
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── config.yml
│ └── feature_request.yml
├── config.yml
├── dependabot.yml
└── workflows
│ ├── build_pull_request.yml
│ ├── open_pull_request.yml
│ ├── release.yml
│ └── update_documentation.yml
├── .gitignore
├── .releaserc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── api
└── revanced-patcher.api
├── assets
├── revanced-headline
│ ├── revanced-headline-vertical-dark.svg
│ └── revanced-headline-vertical-light.svg
└── revanced-logo
│ └── revanced-logo.svg
├── build.gradle.kts
├── docs
├── 1_patcher_intro.md
├── 2_1_setup.md
├── 2_2_1_fingerprinting.md
├── 2_2_patch_anatomy.md
├── 2_patches_intro.md
├── 3_structure_and_conventions.md
├── 4_apis.md
└── README.md
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── package-lock.json
├── package.json
├── settings.gradle.kts
└── src
├── main
├── kotlin
│ └── app
│ │ └── revanced
│ │ └── patcher
│ │ ├── Fingerprint.kt
│ │ ├── InternalApi.kt
│ │ ├── PackageMetadata.kt
│ │ ├── Patcher.kt
│ │ ├── PatcherConfig.kt
│ │ ├── PatcherContext.kt
│ │ ├── PatcherResult.kt
│ │ ├── extensions
│ │ ├── Extensions.kt
│ │ └── InstructionExtensions.kt
│ │ ├── patch
│ │ ├── BytecodePatchContext.kt
│ │ ├── Option.kt
│ │ ├── Patch.kt
│ │ ├── PatchContext.kt
│ │ └── ResourcePatchContext.kt
│ │ └── util
│ │ ├── ClassMerger.kt
│ │ ├── Document.kt
│ │ ├── MethodNavigator.kt
│ │ ├── ProxyClassList.kt
│ │ ├── proxy
│ │ ├── ClassProxy.kt
│ │ └── mutableTypes
│ │ │ ├── MutableAnnotation.kt
│ │ │ ├── MutableAnnotationElement.kt
│ │ │ ├── MutableClass.kt
│ │ │ ├── MutableField.kt
│ │ │ ├── MutableMethod.kt
│ │ │ ├── MutableMethodParameter.kt
│ │ │ └── encodedValue
│ │ │ ├── MutableAnnotationEncodedValue.kt
│ │ │ ├── MutableArrayEncodedValue.kt
│ │ │ ├── MutableBooleanEncodedValue.kt
│ │ │ ├── MutableByteEncodedValue.kt
│ │ │ ├── MutableCharEncodedValue.kt
│ │ │ ├── MutableDoubleEncodedValue.kt
│ │ │ ├── MutableEncodedValue.kt
│ │ │ ├── MutableEnumEncodedValue.kt
│ │ │ ├── MutableFieldEncodedValue.kt
│ │ │ ├── MutableFloatEncodedValue.kt
│ │ │ ├── MutableIntEncodedValue.kt
│ │ │ ├── MutableLongEncodedValue.kt
│ │ │ ├── MutableMethodEncodedValue.kt
│ │ │ ├── MutableMethodHandleEncodedValue.kt
│ │ │ ├── MutableMethodTypeEncodedValue.kt
│ │ │ ├── MutableNullEncodedValue.kt
│ │ │ ├── MutableShortEncodedValue.kt
│ │ │ ├── MutableStringEncodedValue.kt
│ │ │ └── MutableTypeEncodedValue.kt
│ │ └── smali
│ │ ├── ExternalLabel.kt
│ │ └── InlineSmaliCompiler.kt
└── resources
│ └── app
│ └── revanced
│ └── patcher
│ └── version.properties
└── test
└── kotlin
└── app
└── revanced
└── patcher
├── PatcherTest.kt
├── extensions
└── InstructionExtensionsTest.kt
├── patch
├── PatchLoaderTest.kt
├── PatchTest.kt
└── options
│ └── OptionsTest.kt
└── util
└── smali
└── InlineSmaliCompilerTest.kt
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{kt,kts}]
2 | ktlint_code_style = intellij_idea
3 | ktlint_standard_no-wildcard-imports = disabled
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | #
2 | # https://help.github.com/articles/dealing-with-line-endings/
3 | #
4 | # Linux start script should use lf
5 | /gradlew text eol=lf
6 |
7 | # These are Windows script files and should use crlf
8 | *.bat text eol=crlf
9 |
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Bug report
2 | description: Report a bug or an issue.
3 | title: "bug: "
4 | labels: ["Bug report"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 |
10 |
11 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Continuing the legacy of Vanced
67 |
68 |
69 | # ReVanced Patcher bug report
70 |
71 | Before creating a new bug report, please keep the following in mind:
72 |
73 | - **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patcher/issues?q=label%3A%22Bug+report%22).
74 | - **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patcher/blob/main/CONTRIBUTING.md).
75 | - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
76 | - type: textarea
77 | attributes:
78 | label: Bug description
79 | description: |
80 | - Describe your bug in detail
81 | - Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
82 | - Add images and videos if possible
83 | validations:
84 | required: true
85 | - type: textarea
86 | attributes:
87 | label: Error logs
88 | description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
89 | render: shell
90 | - type: textarea
91 | attributes:
92 | label: Solution
93 | description: If applicable, add a possible solution to the bug.
94 | - type: textarea
95 | attributes:
96 | label: Additional context
97 | description: Add additional context here.
98 | - type: checkboxes
99 | id: acknowledgements
100 | attributes:
101 | label: Acknowledgements
102 | description: Your bug report will be closed if you don't follow the checklist below.
103 | options:
104 | - label: I have checked all open and closed bug reports and this is not a duplicate.
105 | required: true
106 | - label: I have chosen an appropriate title.
107 | required: true
108 | - label: All requested information has been provided properly.
109 | required: true
110 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 📃 Documentation
4 | url: https://github.com/revanced/revanced-documentation/
5 | about: Don't know how or where to start? Check out our documentation!
6 | - name: 🗨 Discussions
7 | url: https://github.com/revanced/revanced-suggestions/discussions
8 | about: Got something you think should change or be added? Search for or start a new discussion!
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: ⭐ Feature request
2 | description: Create a detailed request for a new feature.
3 | title: "feat: "
4 | labels: ["Feature request"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 |
10 |
11 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Continuing the legacy of Vanced
67 |
68 |
69 | # ReVanced Patcher feature request
70 |
71 | Before creating a new feature request, please keep the following in mind:
72 |
73 | - **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patcher/issues?q=label%3A%22Feature+request%22).
74 | - **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patcher/blob/main/CONTRIBUTING.md).
75 | - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
76 |
77 | - type: textarea
78 | attributes:
79 | label: Feature description
80 | description: |
81 | - Describe your feature in detail
82 | - Add images, videos, links, examples, references, etc. if possible
83 | - Add the target application name in case you request a new patch
84 | - type: textarea
85 | attributes:
86 | label: Motivation
87 | description: |
88 | A strong motivation is necessary for a feature request to be considered.
89 |
90 | - Why should this feature be implemented?
91 | - What is the explicit use case?
92 | - What are the benefits?
93 | - What makes this feature important?
94 | validations:
95 | required: true
96 | - type: checkboxes
97 | id: acknowledgements
98 | attributes:
99 | label: Acknowledgements
100 | description: Your feature request will be closed if you don't follow the checklist below.
101 | options:
102 | - label: I have checked all open and closed feature requests and this is not a duplicate.
103 | required: true
104 | - label: I have chosen an appropriate title.
105 | required: true
106 | - label: All requested information has been provided properly.
107 | required: true
108 |
--------------------------------------------------------------------------------
/.github/config.yml:
--------------------------------------------------------------------------------
1 | firstPRMergeComment: >
2 | Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | labels: []
5 | directory: /
6 | target-branch: dev
7 | schedule:
8 | interval: monthly
9 |
10 | - package-ecosystem: npm
11 | labels: []
12 | directory: /
13 | target-branch: dev
14 | schedule:
15 | interval: monthly
16 |
17 | - package-ecosystem: gradle
18 | labels: []
19 | directory: /
20 | target-branch: dev
21 | schedule:
22 | interval: monthly
23 |
--------------------------------------------------------------------------------
/.github/workflows/build_pull_request.yml:
--------------------------------------------------------------------------------
1 | name: Build pull request
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | branches:
7 | - dev
8 |
9 | jobs:
10 | release:
11 | name: Build
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 |
19 | - name: Cache Gradle
20 | uses: burrunan/gradle-cache-action@v1
21 |
22 | - name: Build
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 | run: ./gradlew build --no-daemon
26 |
--------------------------------------------------------------------------------
/.github/workflows/open_pull_request.yml:
--------------------------------------------------------------------------------
1 | name: Open a PR to main
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 | workflow_dispatch:
8 |
9 | env:
10 | MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
11 |
12 | jobs:
13 | pull-request:
14 | name: Open pull request
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Open pull request
21 | uses: repo-sync/pull-request@v2
22 | with:
23 | destination_branch: 'main'
24 | pr_title: 'chore: ${{ env.MESSAGE }}'
25 | pr_body: 'This pull request will ${{ env.MESSAGE }}.'
26 | pr_draft: true
27 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - main
8 | - dev
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | permissions:
14 | contents: write
15 | packages: write
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | with:
21 | # Make sure the release step uses its own credentials:
22 | # https://github.com/cycjimmy/semantic-release-action#private-packages
23 | persist-credentials: false
24 | fetch-depth: 0
25 |
26 | - name: Cache Gradle
27 | uses: burrunan/gradle-cache-action@v1
28 |
29 | - name: Build
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 | run: ./gradlew build clean
33 |
34 | - name: Setup Node.js
35 | uses: actions/setup-node@v4
36 | with:
37 | node-version: "lts/*"
38 | cache: 'npm'
39 |
40 | - name: Install dependencies
41 | run: npm install
42 |
43 | - name: Import GPG key
44 | uses: crazy-max/ghaction-import-gpg@v6
45 | with:
46 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
47 | passphrase: ${{ secrets.GPG_PASSPHRASE }}
48 | fingerprint: ${{ vars.GPG_FINGERPRINT }}
49 |
50 | - name: Release
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 | run: npm exec semantic-release
54 |
--------------------------------------------------------------------------------
/.github/workflows/update_documentation.yml:
--------------------------------------------------------------------------------
1 | name: Update documentation
2 |
3 | on:
4 | push:
5 | paths:
6 | - docs/**
7 |
8 | jobs:
9 | trigger:
10 | runs-on: ubuntu-latest
11 | name: Dispatch event to documentation repository
12 | if: github.ref == 'refs/heads/main'
13 | steps:
14 | - uses: peter-evans/repository-dispatch@v3
15 | with:
16 | token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
17 | repository: revanced/revanced-documentation
18 | event-type: update-documentation
19 | client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Java template
2 | # Compiled class file
3 | *.class
4 |
5 | # Log file
6 | *.log
7 |
8 | # BlueJ files
9 | *.ctxt
10 |
11 | # Mobile Tools for Java (J2ME)
12 | .mtj.tmp/
13 |
14 | # Package Files #
15 | *.jar
16 | *.war
17 | *.nar
18 | *.ear
19 | *.zip
20 | *.tar.gz
21 | *.rar
22 |
23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
24 | hs_err_pid*
25 |
26 | ### JetBrains template
27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
28 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
29 |
30 | # User-specific stuff
31 | .idea/**/workspace.xml
32 | .idea/**/tasks.xml
33 | .idea/**/usage.statistics.xml
34 | .idea/**/dictionaries
35 | .idea/**/shelf
36 |
37 | # Generated files
38 | .idea/**/contentModel.xml
39 |
40 | # Sensitive or high-churn files
41 | .idea/**/dataSources/
42 | .idea/**/dataSources.ids
43 | .idea/**/dataSources.local.xml
44 | .idea/**/sqlDataSources.xml
45 | .idea/**/dynamic.xml
46 | .idea/**/uiDesigner.xml
47 | .idea/**/dbnavigator.xml
48 |
49 | # Gradle
50 | .idea/**/gradle.xml
51 | .idea/**/libraries
52 |
53 | # Gradle and Maven with auto-import
54 | # When using Gradle or Maven with auto-import, you should exclude module files,
55 | # since they will be recreated, and may cause churn. Uncomment if using
56 | # auto-import.
57 | .idea/artifacts
58 | .idea/compiler.xml
59 | .idea/jarRepositories.xml
60 | .idea/modules.xml
61 | .idea/*.iml
62 | .idea/modules
63 | *.iml
64 | *.ipr
65 |
66 | # CMake
67 | cmake-build-*/
68 |
69 | # Mongo Explorer plugin
70 | .idea/**/mongoSettings.xml
71 |
72 | # File-based project format
73 | *.iws
74 |
75 | # IntelliJ
76 | out/
77 | .idea/
78 |
79 | # mpeltonen/sbt-idea plugin
80 | .idea_modules/
81 |
82 | # JIRA plugin
83 | atlassian-ide-plugin.xml
84 |
85 | # Cursive Clojure plugin
86 | .idea/replstate.xml
87 |
88 | # Crashlytics plugin (for Android Studio and IntelliJ)
89 | com_crashlytics_export_strings.xml
90 | crashlytics.properties
91 | crashlytics-build.properties
92 | fabric.properties
93 |
94 | # Editor-based Rest Client
95 | .idea/httpRequests
96 |
97 | # Android studio 3.1+ serialized cache file
98 | .idea/caches/build_file_checksums.ser
99 |
100 | ### Gradle template
101 | .gradle
102 | **/build/
103 | !src/**/build/
104 |
105 | # Ignore Gradle GUI config
106 | gradle-app.setting
107 |
108 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
109 | !gradle-wrapper.jar
110 |
111 | # Cache of project
112 | .gradletasknamecache
113 |
114 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
115 | # gradle/wrapper/gradle-wrapper.properties
116 |
117 | # Avoid ignoring test resources
118 | !src/test/resources/*
119 |
120 | # Dependency directories
121 | node_modules/
122 |
123 | # Gradle props, to avoid sharing the gpr key
124 | gradle.properties
125 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "main",
4 | {
5 | "name": "dev",
6 | "prerelease": true
7 | }
8 | ],
9 | "plugins": [
10 | [
11 | "@semantic-release/commit-analyzer", {
12 | "releaseRules": [
13 | { "type": "build", "scope": "Needs bump", "release": "patch" }
14 | ]
15 | }
16 | ],
17 | "@semantic-release/release-notes-generator",
18 | "@semantic-release/changelog",
19 | "gradle-semantic-release-plugin",
20 | [
21 | "@semantic-release/git",
22 | {
23 | "assets": [
24 | "CHANGELOG.md",
25 | "gradle.properties"
26 | ],
27 | "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
28 | }
29 | ],
30 | [
31 | "@saithodev/semantic-release-backmerge",
32 | {
33 | backmergeBranches: [{"from": "main", "to": "dev"}],
34 | clearWorkspace: true
35 | }
36 | ],
37 | [
38 | "@semantic-release/github",
39 | {
40 | successComment: false
41 | }
42 | ]
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Continuing the legacy of Vanced
59 |
60 |
61 | # 👋 Contribution guidelines
62 |
63 | This document describes how to contribute to ReVanced Patcher.
64 |
65 | ## 📖 Resources to help you get started
66 |
67 | - The [documentation](https://github.com/ReVanced/revanced-patcher/tree/docs/docs) contains the fundamentals
68 | of ReVanced Patcher and how to use ReVanced Patcher to create patches
69 | - [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
70 | - [Issues](https://github.com/ReVanced/revanced-patcher/issues) are where we keep track of bugs and feature requests
71 |
72 | ## 🙏 Submitting a feature request
73 |
74 | Features can be requested by opening an issue using the
75 | [Feature request issue template](https://github.com/ReVanced/revanced-patcher/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
76 |
77 | > **Note**
78 | > Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patcher.
79 | > Good motivation has to be provided for a request to be accepted.
80 |
81 | ## 🐞 Submitting a bug report
82 |
83 | If you encounter a bug while using ReVanced Patcher, open an issue using the
84 | [Bug report issue template](https://github.com/ReVanced/revanced-patcher/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
85 |
86 | ## 📝 How to contribute
87 |
88 | 1. Before contributing, it is recommended to open an issue to discuss your change
89 | with the maintainers of ReVanced Patcher. This will help you determine whether your change is acceptable
90 | and whether it is worth your time to implement it
91 | 2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
92 | 3. Commit your changes
93 | 4. Submit a pull request to the `dev` branch of the repository and reference issues
94 | that your pull request closes in the description of your pull request
95 | 5. Our team will review your pull request and provide feedback. Once your pull request is approved,
96 | it will be merged into the `dev` branch and will be included in the next release of ReVanced Patcher
97 |
98 | ❤️ Thank you for considering contributing to ReVanced Patcher,
99 | ReVanced
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Continuing the legacy of Vanced
59 |
60 |
61 | # 💉 ReVanced Patcher
62 |
63 | 
64 | 
65 |
66 | ReVanced Patcher used to patch Android applications.
67 |
68 | ## ❓ About
69 |
70 | ReVanced Patcher is a library that is used to patch Android applications.
71 | It powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager),
72 | [ReVanced CLI](https://github.com/ReVanced/revanced-cli)
73 | and [ReVanced Library](https://github.com/ReVanced/revanced-library) and a rich set of patches have been developed
74 | using ReVanced Patcher in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository.
75 |
76 | ## 💪 Features
77 |
78 | Some of the features the ReVanced Patcher provides are:
79 |
80 | - 🔧 **Patch Dalvik VM bytecode**: Disassemble and assemble Dalvik bytecode
81 | - 📦 **Patch APK resources**: Decode and build Android APK resources
82 | - 📂 **Patch arbitrary APK files**: Read and write arbitrary files directly from and to APK files
83 | - 🧩 **Write modular patches**: Extensive API to write modular patches that can patch Dalvik VM bytecode,
84 | APK resources and arbitrary APK files
85 |
86 | ## 🚀 How to get started
87 |
88 | To use ReVanced Patcher in your project, follow these steps:
89 |
90 | 1. [Add the repository](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
91 | to your project
92 | 2. Add the dependency to your project:
93 |
94 | ```kt
95 | dependencies {
96 | implementation("app.revanced:revanced-patcher:{$version}")
97 | }
98 | ```
99 |
100 | For a minimal project configuration,
101 | see [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template).
102 |
103 | ## 📚 Everything else
104 |
105 | ### 📙 Contributing
106 |
107 | Thank you for considering contributing to ReVanced Patcher.
108 | You can find the contribution guidelines [here](CONTRIBUTING.md).
109 |
110 | ### 🛠️ Building
111 |
112 | To build ReVanced Patcher,
113 | you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
114 |
115 | ### 📃 Documentation
116 |
117 | The documentation contains the fundamentals of ReVanced Patcher and how to use ReVanced Patcher to create patches.
118 | You can find it [here](https://github.com/ReVanced/revanced-patcher/tree/main/docs).
119 |
120 | ## 📜 Licence
121 |
122 | ReVanced Patcher is licensed under the GPLv3 license. Please see the [licence file](LICENSE) for more information.
123 | [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patcher as long as you track changes/dates in source files.
124 | Any modifications to ReVanced Patcher must also be made available under the GPL,
125 | along with build & install instructions.
126 |
--------------------------------------------------------------------------------
/assets/revanced-headline/revanced-headline-vertical-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/revanced-headline/revanced-headline-vertical-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/revanced-logo/revanced-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin)
5 | alias(libs.plugins.binary.compatibility.validator)
6 | `maven-publish`
7 | signing
8 | }
9 |
10 | group = "app.revanced"
11 |
12 | tasks {
13 | processResources {
14 | expand("projectVersion" to project.version)
15 | }
16 |
17 | test {
18 | useJUnitPlatform()
19 | testLogging {
20 | events("PASSED", "SKIPPED", "FAILED")
21 | }
22 | }
23 | }
24 |
25 | repositories {
26 | mavenCentral()
27 | google()
28 | maven {
29 | // A repository must be specified for some reason. "registry" is a dummy.
30 | url = uri("https://maven.pkg.github.com/revanced/registry")
31 | credentials {
32 | username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
33 | password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
34 | }
35 | }
36 | }
37 |
38 | dependencies {
39 | // TODO: Convert project to KMP.
40 | compileOnly(libs.android) {
41 | // Exclude, otherwise the org.w3c.dom API breaks.
42 | exclude(group = "xerces", module = "xmlParserAPIs")
43 | }
44 |
45 | implementation(libs.apktool.lib)
46 | implementation(libs.kotlin.reflect)
47 | implementation(libs.kotlinx.coroutines.core)
48 | implementation(libs.multidexlib2)
49 | implementation(libs.smali)
50 | implementation(libs.xpp3)
51 |
52 | testImplementation(libs.mockk)
53 | testImplementation(libs.kotlin.test)
54 | }
55 |
56 | kotlin {
57 | compilerOptions {
58 | jvmTarget.set(JvmTarget.JVM_11)
59 |
60 | freeCompilerArgs = listOf("-Xcontext-receivers")
61 | }
62 | }
63 |
64 | java {
65 | targetCompatibility = JavaVersion.VERSION_11
66 |
67 | withSourcesJar()
68 | }
69 |
70 | publishing {
71 | repositories {
72 | maven {
73 | name = "GitHubPackages"
74 | url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
75 | credentials {
76 | username = System.getenv("GITHUB_ACTOR")
77 | password = System.getenv("GITHUB_TOKEN")
78 | }
79 | }
80 | }
81 |
82 | publications {
83 | create("revanced-patcher-publication") {
84 | from(components["java"])
85 |
86 | version = project.version.toString()
87 |
88 | pom {
89 | name = "ReVanced Patcher"
90 | description = "Patcher used by ReVanced."
91 | url = "https://revanced.app"
92 |
93 | licenses {
94 | license {
95 | name = "GNU General Public License v3.0"
96 | url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
97 | }
98 | }
99 | developers {
100 | developer {
101 | id = "ReVanced"
102 | name = "ReVanced"
103 | email = "contact@revanced.app"
104 | }
105 | }
106 | scm {
107 | connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
108 | developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
109 | url = "https://github.com/revanced/revanced-patcher"
110 | }
111 | }
112 | }
113 | }
114 | }
115 |
116 | signing {
117 | useGpgCmd()
118 | sign(publishing.publications["revanced-patcher-publication"])
119 | }
120 |
--------------------------------------------------------------------------------
/docs/1_patcher_intro.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Continuing the legacy of Vanced
59 |
60 |
61 | # 💉 Introduction to ReVanced Patcher
62 |
63 | To create patches for Android apps, it is recommended to know the basic concept of ReVanced Patcher.
64 |
65 | ## 📙 How it works
66 |
67 | ReVanced Patcher is a library that allows modifying Android apps by applying patches.
68 | It is built on top of [Smali](https://github.com/google/smali) for bytecode manipulation and [Androlib (Apktool)](https://github.com/iBotPeaches/Apktool)
69 | for resource decoding and encoding.
70 |
71 | ReVanced Patcher receives a list of patches and applies them to a given APK file.
72 | It then returns the modified components of the APK file, such as modified dex files and resources,
73 | that can be repackaged into a new APK file.
74 |
75 | ReVanced Patcher has a simple API that allows you to load patches from RVP (JAR or DEX container) files
76 | and apply them to an APK file. Later on, you will learn how to create patches.
77 |
78 | ```kt
79 | val patches = loadPatchesFromJar(setOf(File("revanced-patches.rvp")))
80 |
81 | val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { patcher ->
82 | // Here you can access metadata about the APK file through patcher.context.packageMetadata
83 | // such as package name, version code, version name, etc.
84 |
85 | // Add patches.
86 | patcher += patches
87 |
88 | // Execute the patches.
89 | runBlocking {
90 | patcher().collect { patchResult ->
91 | if (patchResult.exception != null)
92 | logger.info { "\"${patchResult.patch}\" failed:\n${patchResult.exception}" }
93 | else
94 | logger.info { "\"${patchResult.patch}\" succeeded" }
95 | }
96 | }
97 |
98 | // Compile and save the patched APK file components.
99 | patcher.get()
100 | }
101 |
102 | // The result of the patcher contains the modified components of the APK file that can be repackaged into a new APK file.
103 | val dexFiles = patcherResult.dexFiles
104 | val resources = patcherResult.resources
105 | ```
106 |
107 | ## ⏭️ What's next
108 |
109 | The next page teaches the fundamentals of ReVanced Patches.
110 |
111 | Continue: [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
112 |
--------------------------------------------------------------------------------
/docs/2_1_setup.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Continuing the legacy of Vanced
59 |
60 |
61 | # 👶 Setting up a development environment
62 |
63 | To start developing patches with ReVanced Patcher, you must prepare a development environment.
64 |
65 | ## 📝 Prerequisites
66 |
67 | - A Java IDE with Kotlin support, such as [IntelliJ IDEA](https://www.jetbrains.com/idea/)
68 | - Knowledge of Java, [Kotlin](https://kotlinlang.org), and [Dalvik bytecode](https://source.android.com/docs/core/runtime/dalvik-bytecode)
69 | - Android reverse engineering skills and tools such as [jadx](https://github.com/skylot/jadx)
70 |
71 | ## 🏃 Prepare the environment
72 |
73 | Throughout the documentation, [ReVanced Patches](https://github.com/revanced/revanced-patches) will be used as an example project.
74 |
75 | > [!NOTE]
76 | > To start a fresh project,
77 | > you can use the [ReVanced Patches template](https://github.com/revanced/revanced-patches-template).
78 |
79 | 1. Clone the repository
80 |
81 | ```bash
82 | git clone https://github.com/revanced/revanced-patches && cd revanced-patches
83 | ```
84 |
85 | 2. Build the project
86 |
87 | ```bash
88 | ./gradlew build
89 | ```
90 |
91 | > [!NOTE]
92 | > If the build fails due to authentication, you may need to authenticate to GitHub Packages.
93 | > Create a PAT with the scope `read:packages` [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced) and add your token to ~/.gradle/gradle.properties.
94 | >
95 | > Example `gradle.properties` file:
96 | >
97 | > ```properties
98 | > gpr.user = user
99 | > gpr.key = key
100 | > ```
101 |
102 | 3. Open the project in your IDE
103 |
104 | > [!TIP]
105 | > It is a good idea to set up a complete development environment for ReVanced, so that you can also test your patches
106 | > by following the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
107 |
108 | ## ⏭️ What's next
109 |
110 | The next page will go into details about a ReVanced patch.
111 |
112 | Continue: [🧩 Anatomy of a patch](2_2_patch_anatomy.md)
113 |
--------------------------------------------------------------------------------
/docs/2_patches_intro.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Continuing the legacy of Vanced
59 |
60 |
61 | # 🧩 Introduction to ReVanced Patches
62 |
63 | Learn the basic concepts of ReVanced Patcher and how to create patches.
64 |
65 | ## 📙 Fundamentals
66 |
67 | A patch is a piece of code that modifies an Android application.
68 | There are multiple types of patches. Each type can modify a different part of the APK, such as the Dalvik VM bytecode,
69 | the APK resources, or arbitrary files in the APK:
70 |
71 | - A `BytecodePatch` modifies the Dalvik VM bytecode
72 | - A `ResourcePatch` modifies (decoded) resources
73 | - A `RawResourcePatch` modifies arbitrary files
74 |
75 | Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies
76 | before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way.
77 |
78 | The `execute` function is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed.
79 | The `execute` function receives an instance of a context object that provides access to the APK.
80 | The patch can use this context to modify the APK.
81 |
82 | Each type of context provides different APIs to modify the APK. For example, the `BytecodePatchContext` provides APIs
83 | to modify the Dalvik VM bytecode, while the `ResourcePatchContext` provides APIs to modify resources.
84 |
85 | The difference between `ResourcePatch` and `RawResourcePatch` is that ReVanced Patcher will decode the resources
86 | if it is supplied a `ResourcePatch` for execution or if any patch depends on a `ResourcePatch`
87 | and will not decode the resources before executing `RawResourcePatch`.
88 | Both, `ResourcePatch` and `RawResourcePatch` can modify arbitrary files in the APK,
89 | whereas only `ResourcePatch` can modify decoded resources. The choice of which type to use depends on the use case.
90 | Decoding and building resources is a time- and resource-consuming,
91 | so if the patch does not need to modify decoded resources, it is better to use `RawResourcePatch` or `BytecodePatch`.
92 |
93 | Example of patches:
94 |
95 | ```kt
96 | @Surpress("unused")
97 | val bytecodePatch = bytecodePatch {
98 | execute {
99 | // More about this on the next page of the documentation.
100 | }
101 | }
102 |
103 | @Surpress("unused")
104 | val rawResourcePatch = rawResourcePatch {
105 | execute {
106 | // More about this on the next page of the documentation.
107 | }
108 | }
109 |
110 | @Surpress("unused")
111 | val resourcePatch = resourcePatch {
112 | execute {
113 | // More about this on the next page of the documentation.
114 | }
115 | }
116 | ```
117 |
118 | > [!TIP]
119 | > To see real-world examples of patches,
120 | > check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
121 |
122 | ## ⏭️ Whats next
123 |
124 | The next page will guide you through creating a development environment for creating patches.
125 |
126 | Continue: [👶 Setting up a development environment](2_1_setup.md)
127 |
--------------------------------------------------------------------------------
/docs/3_structure_and_conventions.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Continuing the legacy of Vanced
59 |
60 |
61 | # 📜 Project structure and conventions
62 |
63 | Over time, a specific project structure and conventions have been established.
64 |
65 | ## 📁 File structure
66 |
67 | Patches are organized in a specific way. The file structure looks as follows:
68 |
69 | ```text
70 | 📦your.patches.app.category
71 | ├ 🔍Fingerprints.kt
72 | └ 🧩SomePatch.kt
73 | ```
74 |
75 | > [!NOTE]
76 | > Moving fingerprints to a separate file isn't strictly necessary, but it helps the organization when a patch uses multiple fingerprints.
77 |
78 | ## 📙 Conventions
79 |
80 | - 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `Remove ads`.
81 | If a patch changes the color of a button, name it `Change button color`
82 | - 🔥 Write the patch description in the third person, present tense, and end it with a period.
83 | If a patch removes ads, the description can be omitted because of redundancy,
84 | but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._
85 | - 🔥 Write patches with modularity and reusability in mind. Patches can depend on each other,
86 | so it is important to write patches in a way that can be used in different contexts.
87 | - 🔥🔥 Keep patches as minimal as possible. This reduces the risk of failing patches.
88 | Instead of involving many abstract changes in one patch or writing entire methods or classes in a patch,
89 | you can write code in extensions. An extension is a precompiled DEX file that is merged into the patched app
90 | before this patch is executed.
91 | Patches can then reference methods and classes from extensions.
92 | A real-world example of extensions can be found in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository
93 | - 🔥🔥🔥 Do not overload a fingerprint with information about a method that's likely to change.
94 | In the example of an obfuscated method, it's better to fingerprint the method by its return type
95 | and parameters rather than its name because the name is likely to change. An intelligent selection
96 | of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates.
97 | - 🔥🔥🔥 Document your patches. Patches are abstract, so it is important to document parts of the code
98 | that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks
99 | of instructions that are modified or added to a method
100 |
101 | ## ⏭️ What's next
102 |
103 | The next page discusses useful APIs for patch development.
104 |
105 | Continue: [💪 Advanced APIs](4_apis.md)
106 |
--------------------------------------------------------------------------------
/docs/4_apis.md:
--------------------------------------------------------------------------------
1 | # 💪 Advanced APIs
2 |
3 | A handful of APIs are available to make patch development easier and more efficient.
4 |
5 | ## 📙 Overview
6 |
7 | 1. 👹 Create mutable replacements of classes with `proxy(ClassDef)`
8 | 2. 🔍 Find and create mutable replaces with `classBy(Predicate)`
9 | 3. 🏃 Navigate method calls recursively by index with `navigate(Method)`
10 | 4. 💾 Read and write resource files with `get(String, Boolean)` and `delete(String)`
11 | 5. 📃 Read and write DOM files using `document(String)` and `document(InputStream)`
12 |
13 | ### 🧰 APIs
14 |
15 | #### 👹 `proxy(ClassDef)`
16 |
17 | By default, the classes are immutable, meaning they cannot be modified.
18 | To make a class mutable, use the `proxy(ClassDef)` function.
19 | This function creates a lazy mutable copy of the class definition.
20 | Accessing the property will replace the original class definition with the mutable copy,
21 | thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy.
22 |
23 | ```kt
24 | execute {
25 | val mutableClass = proxy(classDef)
26 | mutableClass.methods.add(Method())
27 | }
28 | ```
29 |
30 | #### 🔍 `classBy(Predicate)`
31 |
32 | The `classBy(Predicate)` function is an alternative to finding and creating mutable classes by a predicate.
33 | It automatically proxies the class definition, making it mutable.
34 |
35 | ```kt
36 | execute {
37 | // Alternative to proxy(classes.find { it.name == "Lcom/example/MyClass;" })?.classDef
38 | val classDef = classBy { it.name == "Lcom/example/MyClass;" }?.classDef
39 | }
40 | ```
41 |
42 | #### 🏃 `navigate(Method).at(index)`
43 |
44 | The `navigate(Method)` function allows you to navigate method calls recursively by index.
45 |
46 | ```kt
47 | execute {
48 | // Sequentially navigate to the instructions at index 1 within 'someMethod'.
49 | val method = navigate(someMethod).to(1).original() // original() returns the original immutable method.
50 |
51 | // Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'.
52 | // stop() returns the mutable copy of the method.
53 | val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop()
54 |
55 | // Alternatively, to stop(), you can delegate the method to a variable.
56 | val method by navigate(someMethod).to(1)
57 |
58 | // You can chain multiple calls to at() to navigate deeper into the method.
59 | val method by navigate(someMethod).to(1).to(2, 3, 4).to(5)
60 | }
61 | ```
62 |
63 | #### 💾 `get(String, Boolean)` and `delete(String)`
64 |
65 | The `get(String, Boolean)` function returns a `File` object that can be used to read and write resource files.
66 |
67 | ```kt
68 | execute {
69 | val file = get("res/values/strings.xml")
70 | val content = file.readText()
71 | file.writeText(content)
72 | }
73 | ```
74 |
75 | The `delete` function can mark files for deletion when the APK is rebuilt.
76 |
77 | ```kt
78 | execute {
79 | delete("res/values/strings.xml")
80 | }
81 | ```
82 |
83 | #### 📃 `document(String)` and `document(InputStream)`
84 |
85 | The `document` function is used to read and write DOM files.
86 |
87 | ```kt
88 | execute {
89 | document("res/values/strings.xml").use { document ->
90 | val element = doc.createElement("string").apply {
91 | textContent = "Hello, World!"
92 | }
93 | document.documentElement.appendChild(element)
94 | }
95 | }
96 | ```
97 |
98 | You can also read documents from an `InputStream`:
99 |
100 | ```kt
101 | execute {
102 | val inputStream = classLoader.getResourceAsStream("some.xml")
103 | document(inputStream).use { document ->
104 | // ...
105 | }
106 | }
107 | ```
108 |
109 | ## 🎉 Afterword
110 |
111 | ReVanced Patcher is a powerful library to patch Android applications, offering a rich set of APIs to develop patches
112 | that outlive app updates. Patches make up ReVanced; without you, the community of patch developers,
113 | ReVanced would not be what it is today. We hope that this documentation has been helpful to you
114 | and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help,
115 | talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or
116 | feature request,
117 | ReVanced
118 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Continuing the legacy of Vanced
59 |
60 |
61 | # 💉 Documentation of ReVanced Patcher
62 |
63 | This documentation contains the fundamentals of ReVanced Patcher and how to use ReVanced Patcher to create patches
64 |
65 | ## 📖 Table of content
66 |
67 | 1. [💉 Introduction to ReVanced Patcher](1_patcher_intro.md)
68 | 2. [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
69 | 1. [👶 Setting up a development environment](2_1_setup.md)
70 | 2. [🧩 Anatomy of a ReVanced patch](2_2_patch_anatomy.md)
71 | 1. [🔎 Fingerprinting](2_2_1_fingerprinting.md)
72 | 3. [📜 Project structure and conventions](3_structure_and_conventions.md)
73 | 4. [💪 Advanced APIs](4_apis.md)
74 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.parallel = true
2 | org.gradle.caching = true
3 | version = 21.0.0
4 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | android = "4.1.1.4"
3 | apktool-lib = "2.9.3"
4 | binary-compatibility-validator = "0.15.1"
5 | kotlin = "2.0.0"
6 | kotlinx-coroutines-core = "1.8.1"
7 | mockk = "1.13.10"
8 | multidexlib2 = "3.0.3.r3"
9 | # Tracking https://github.com/google/smali/issues/64.
10 | #noinspection GradleDependency
11 | smali = "3.0.5"
12 | xpp3 = "1.1.4c"
13 |
14 | [libraries]
15 | android = { module = "com.google.android:android", version.ref = "android" }
16 | apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
17 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
18 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
19 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
20 | mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
21 | multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
22 | smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
23 | xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
24 |
25 | [plugins]
26 | binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
27 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
28 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReVanced/revanced-patcher/ead701bdaf51f30ccefe846471bc2d3b550b7bdb/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@saithodev/semantic-release-backmerge": "^4.0.1",
4 | "@semantic-release/changelog": "^6.0.3",
5 | "@semantic-release/git": "^10.0.1",
6 | "gradle-semantic-release-plugin": "^1.10.1",
7 | "semantic-release": "^24.1.2"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "revanced-patcher"
2 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/InternalApi.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher
2 |
3 | @RequiresOptIn(
4 | level = RequiresOptIn.Level.ERROR,
5 | message = "This is an internal API, don't rely on it.",
6 | )
7 | annotation class InternalApi
8 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/PackageMetadata.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher
2 |
3 | import brut.androlib.apk.ApkInfo
4 |
5 | /**
6 | * Metadata about a package.
7 | *
8 | * @param apkInfo The [ApkInfo] of the apk file.
9 | */
10 | class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) {
11 | lateinit var packageName: String
12 | internal set
13 |
14 | lateinit var packageVersion: String
15 | internal set
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/Patcher.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher
2 |
3 | import app.revanced.patcher.patch.*
4 | import kotlinx.coroutines.flow.flow
5 | import java.io.Closeable
6 | import java.util.logging.Logger
7 |
8 | /**
9 | * A Patcher.
10 | *
11 | * @param config The configuration to use for the patcher.
12 | */
13 | class Patcher(private val config: PatcherConfig) : Closeable {
14 | private val logger = Logger.getLogger(this::class.java.name)
15 |
16 | /**
17 | * The context containing the current state of the patcher.
18 | */
19 | val context = PatcherContext(config)
20 |
21 | init {
22 | context.resourceContext.decodeResources(ResourcePatchContext.ResourceMode.NONE)
23 | }
24 |
25 | /**
26 | * Add patches.
27 | *
28 | * @param patches The patches to add.
29 | */
30 | operator fun plusAssign(patches: Set>) {
31 | // Add all patches to the executablePatches set.
32 | context.executablePatches += patches
33 |
34 | // Add all patches and their dependencies to the allPatches set.
35 | patches.forEach { patch ->
36 | fun Patch<*>.addRecursively() =
37 | also(context.allPatches::add).dependencies.forEach(Patch<*>::addRecursively)
38 |
39 | patch.addRecursively()
40 | }
41 |
42 | context.allPatches.let { allPatches ->
43 | // Check, if what kind of resource mode is required.
44 | config.resourceMode = if (allPatches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) {
45 | ResourcePatchContext.ResourceMode.FULL
46 | } else if (allPatches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) {
47 | ResourcePatchContext.ResourceMode.RAW_ONLY
48 | } else {
49 | ResourcePatchContext.ResourceMode.NONE
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * Execute added patches.
56 | *
57 | * @return A flow of [PatchResult]s.
58 | */
59 | operator fun invoke() = flow {
60 | fun Patch<*>.execute(
61 | executedPatches: LinkedHashMap, PatchResult>,
62 | ): PatchResult {
63 | // If the patch was executed before or failed, return it's the result.
64 | executedPatches[this]?.let { patchResult ->
65 | patchResult.exception ?: return patchResult
66 |
67 | return PatchResult(this, PatchException("The patch '$this' failed previously"))
68 | }
69 |
70 | // Recursively execute all dependency patches.
71 | dependencies.forEach { dependency ->
72 | dependency.execute(executedPatches).exception?.let {
73 | return PatchResult(
74 | this,
75 | PatchException(
76 | "The patch \"$this\" depends on \"$dependency\", which raised an exception:\n${it.stackTraceToString()}",
77 | ),
78 | )
79 | }
80 | }
81 |
82 | // Execute the patch.
83 | return try {
84 | execute(context)
85 |
86 | PatchResult(this)
87 | } catch (exception: PatchException) {
88 | PatchResult(this, exception)
89 | } catch (exception: Exception) {
90 | PatchResult(this, PatchException(exception))
91 | }.also { executedPatches[this] = it }
92 | }
93 |
94 | // Prevent decoding the app manifest twice if it is not needed.
95 | if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
96 | context.resourceContext.decodeResources(config.resourceMode)
97 | }
98 |
99 | logger.info("Initializing lookup maps")
100 |
101 | // Accessing the lazy lookup maps to initialize them.
102 | context.bytecodeContext.lookupMaps
103 |
104 | logger.info("Executing patches")
105 |
106 | val executedPatches = LinkedHashMap, PatchResult>()
107 |
108 | context.executablePatches.sortedBy { it.name }.forEach { patch ->
109 | val patchResult = patch.execute(executedPatches)
110 |
111 | // If an exception occurred or the patch has no finalize block, emit the result.
112 | if (patchResult.exception != null || patch.finalizeBlock == null) {
113 | emit(patchResult)
114 | }
115 | }
116 |
117 | val succeededPatchesWithFinalizeBlock = executedPatches.values.filter {
118 | it.exception == null && it.patch.finalizeBlock != null
119 | }
120 |
121 | succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult ->
122 | val patch = executionResult.patch
123 |
124 | val result =
125 | try {
126 | patch.finalize(context)
127 |
128 | executionResult
129 | } catch (exception: PatchException) {
130 | PatchResult(patch, exception)
131 | } catch (exception: Exception) {
132 | PatchResult(patch, PatchException(exception))
133 | }
134 |
135 | if (result.exception != null) {
136 | emit(
137 | PatchResult(
138 | patch,
139 | PatchException(
140 | "The patch \"$patch\" raised an exception: ${result.exception.stackTraceToString()}",
141 | result.exception,
142 | ),
143 | ),
144 | )
145 | } else if (patch in context.executablePatches) {
146 | emit(result)
147 | }
148 | }
149 | }
150 |
151 | override fun close() = context.close()
152 |
153 | /**
154 | * Compile and save patched APK files.
155 | *
156 | * @return The [PatcherResult] containing the patched APK files.
157 | */
158 | @OptIn(InternalApi::class)
159 | fun get() = PatcherResult(context.bytecodeContext.get(), context.resourceContext.get())
160 | }
161 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/PatcherConfig.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher
2 |
3 | import app.revanced.patcher.patch.ResourcePatchContext
4 | import brut.androlib.Config
5 | import java.io.File
6 | import java.util.logging.Logger
7 |
8 | /**
9 | * The configuration for the patcher.
10 | *
11 | * @param apkFile The apk file to patch.
12 | * @param temporaryFilesPath A path to a folder to store temporary files in.
13 | * @param aaptBinaryPath A path to a custom aapt binary.
14 | * @param frameworkFileDirectory A path to the directory to cache the framework file in.
15 | */
16 | class PatcherConfig(
17 | internal val apkFile: File,
18 | private val temporaryFilesPath: File = File("revanced-temporary-files"),
19 | aaptBinaryPath: String? = null,
20 | frameworkFileDirectory: String? = null,
21 | ) {
22 | private val logger = Logger.getLogger(PatcherConfig::class.java.name)
23 |
24 | /**
25 | * The mode to use for resource decoding and compiling.
26 | *
27 | * @see ResourcePatchContext.ResourceMode
28 | */
29 | internal var resourceMode = ResourcePatchContext.ResourceMode.NONE
30 |
31 | /**
32 | * The configuration for decoding and compiling resources.
33 | */
34 | internal val resourceConfig =
35 | Config.getDefaultConfig().apply {
36 | useAapt2 = true
37 | aaptPath = aaptBinaryPath ?: ""
38 | frameworkDirectory = frameworkFileDirectory
39 | }
40 |
41 | /**
42 | * The path to the temporary apk files directory.
43 | */
44 | internal val apkFiles = temporaryFilesPath.resolve("apk")
45 |
46 | /**
47 | * The path to the temporary patched files directory.
48 | */
49 | internal val patchedFiles = temporaryFilesPath.resolve("patched")
50 |
51 | /**
52 | * Initialize the temporary files' directories.
53 | * This will delete the existing temporary files directory if it exists.
54 | */
55 | internal fun initializeTemporaryFilesDirectories() {
56 | temporaryFilesPath.apply {
57 | if (exists()) {
58 | logger.info("Deleting existing temporary files directory")
59 |
60 | if (!deleteRecursively()) {
61 | logger.severe("Failed to delete existing temporary files directory")
62 | }
63 | }
64 | }
65 |
66 | apkFiles.mkdirs()
67 | patchedFiles.mkdirs()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/PatcherContext.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher
2 |
3 | import app.revanced.patcher.patch.BytecodePatchContext
4 | import app.revanced.patcher.patch.Patch
5 | import app.revanced.patcher.patch.ResourcePatchContext
6 | import brut.androlib.apk.ApkInfo
7 | import brut.directory.ExtFile
8 | import java.io.Closeable
9 |
10 | /**
11 | * A context for the patcher containing the current state of the patcher.
12 | *
13 | * @param config The configuration for the patcher.
14 | */
15 | @Suppress("MemberVisibilityCanBePrivate")
16 | class PatcherContext internal constructor(config: PatcherConfig): Closeable {
17 | /**
18 | * [PackageMetadata] of the supplied [PatcherConfig.apkFile].
19 | */
20 | val packageMetadata = PackageMetadata(ApkInfo(ExtFile(config.apkFile)))
21 |
22 | /**
23 | * The set of [Patch]es.
24 | */
25 | internal val executablePatches = mutableSetOf>()
26 |
27 | /**
28 | * The set of all [Patch]es and their dependencies.
29 | */
30 | internal val allPatches = mutableSetOf>()
31 |
32 | /**
33 | * The context for patches containing the current state of the resources.
34 | */
35 | internal val resourceContext = ResourcePatchContext(packageMetadata, config)
36 |
37 | /**
38 | * The context for patches containing the current state of the bytecode.
39 | */
40 | internal val bytecodeContext = BytecodePatchContext(config)
41 |
42 | override fun close() = bytecodeContext.close()
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/PatcherResult.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher
2 |
3 | import java.io.File
4 | import java.io.InputStream
5 |
6 | /**
7 | * The result of a patcher.
8 | *
9 | * @param dexFiles The patched dex files.
10 | * @param resources The patched resources.
11 | */
12 | @Suppress("MemberVisibilityCanBePrivate")
13 | class PatcherResult internal constructor(
14 | val dexFiles: Set,
15 | val resources: PatchedResources?,
16 | ) {
17 |
18 | /**
19 | * A dex file.
20 | *
21 | * @param name The original name of the dex file.
22 | * @param stream The dex file as [InputStream].
23 | */
24 | class PatchedDexFile internal constructor(val name: String, val stream: InputStream)
25 |
26 | /**
27 | * The resources of a patched apk.
28 | *
29 | * @param resourcesApk The compiled resources.apk file.
30 | * @param otherResources The directory containing other resources files.
31 | * @param doNotCompress List of files that should not be compressed.
32 | * @param deleteResources List of resources that should be deleted.
33 | */
34 | class PatchedResources internal constructor(
35 | val resourcesApk: File?,
36 | val otherResources: File?,
37 | val doNotCompress: Set,
38 | val deleteResources: Set,
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.extensions
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
4 |
5 | /**
6 | * Create a label for the instruction at given index.
7 | *
8 | * @param index The index to create the label for the instruction at.
9 | * @return The label.
10 | */
11 | fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.patch
2 |
3 | import app.revanced.patcher.InternalApi
4 | import app.revanced.patcher.PatcherConfig
5 | import app.revanced.patcher.PatcherResult
6 | import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
7 | import app.revanced.patcher.util.ClassMerger.merge
8 | import app.revanced.patcher.util.MethodNavigator
9 | import app.revanced.patcher.util.ProxyClassList
10 | import app.revanced.patcher.util.proxy.ClassProxy
11 | import com.android.tools.smali.dexlib2.Opcode
12 | import com.android.tools.smali.dexlib2.Opcodes
13 | import com.android.tools.smali.dexlib2.iface.ClassDef
14 | import com.android.tools.smali.dexlib2.iface.DexFile
15 | import com.android.tools.smali.dexlib2.iface.Method
16 | import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
17 | import com.android.tools.smali.dexlib2.iface.reference.MethodReference
18 | import com.android.tools.smali.dexlib2.iface.reference.StringReference
19 | import lanchon.multidexlib2.BasicDexFileNamer
20 | import lanchon.multidexlib2.DexIO
21 | import lanchon.multidexlib2.MultiDexIO
22 | import lanchon.multidexlib2.RawDexIO
23 | import java.io.Closeable
24 | import java.io.FileFilter
25 | import java.util.*
26 | import java.util.logging.Logger
27 |
28 | /**
29 | * A context for patches containing the current state of the bytecode.
30 | *
31 | * @param config The [PatcherConfig] used to create this context.
32 | */
33 | @Suppress("MemberVisibilityCanBePrivate")
34 | class BytecodePatchContext internal constructor(private val config: PatcherConfig) :
35 | PatchContext>,
36 | Closeable {
37 | private val logger = Logger.getLogger(this::javaClass.name)
38 |
39 | /**
40 | * [Opcodes] of the supplied [PatcherConfig.apkFile].
41 | */
42 | internal val opcodes: Opcodes
43 |
44 | /**
45 | * The list of classes.
46 | */
47 | val classes = ProxyClassList(
48 | MultiDexIO.readDexFile(
49 | true,
50 | config.apkFile,
51 | BasicDexFileNamer(),
52 | null,
53 | null,
54 | ).also { opcodes = it.opcodes }.classes.toMutableList(),
55 | )
56 |
57 | /**
58 | * The lookup maps for methods and the class they are a member of from the [classes].
59 | */
60 | internal val lookupMaps by lazy { LookupMaps(classes) }
61 |
62 | /**
63 | * Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
64 | * If no extension is present, the function will return early.
65 | *
66 | * @param bytecodePatch The [BytecodePatch] to merge the extension of.
67 | */
68 | internal fun mergeExtension(bytecodePatch: BytecodePatch) {
69 | bytecodePatch.extensionInputStream?.get()?.use { extensionStream ->
70 | RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
71 | val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
72 | logger.fine { "Adding class \"$classDef\"" }
73 |
74 | classes += classDef
75 | lookupMaps.classesByType[classDef.type] = classDef
76 |
77 | return@forEach
78 | }
79 |
80 | logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
81 |
82 | existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
83 | // If the class was merged, replace the original class with the merged class.
84 | if (mergedClass === existingClass) {
85 | return@let
86 | }
87 |
88 | classes -= existingClass
89 | classes += mergedClass
90 | }
91 | }
92 | } ?: logger.fine("No extension to merge")
93 | }
94 |
95 | /**
96 | * Find a class with a predicate.
97 | *
98 | * @param predicate A predicate to match the class.
99 | * @return A proxy for the first class that matches the predicate.
100 | */
101 | fun classBy(predicate: (ClassDef) -> Boolean) =
102 | classes.proxyPool.find { predicate(it.immutableClass) } ?: classes.find(predicate)?.let { proxy(it) }
103 |
104 | /**
105 | * Proxy the class to allow mutation.
106 | *
107 | * @param classDef The class to proxy.
108 | *
109 | * @return A proxy for the class.
110 | */
111 | fun proxy(classDef: ClassDef) = classes.proxyPool.find {
112 | it.immutableClass.type == classDef.type
113 | } ?: ClassProxy(classDef).also { classes.proxyPool.add(it) }
114 |
115 | /**
116 | * Navigate a method.
117 | *
118 | * @param method The method to navigate.
119 | *
120 | * @return A [MethodNavigator] for the method.
121 | */
122 | fun navigate(method: MethodReference) = MethodNavigator(method)
123 |
124 | /**
125 | * Compile bytecode from the [BytecodePatchContext].
126 | *
127 | * @return The compiled bytecode.
128 | */
129 | @InternalApi
130 | override fun get(): Set {
131 | logger.info("Compiling patched dex files")
132 |
133 | // Free up memory before compiling the dex files.
134 | lookupMaps.close()
135 |
136 | val patchedDexFileResults =
137 | config.patchedFiles.resolve("dex").also {
138 | it.deleteRecursively() // Make sure the directory is empty.
139 | it.mkdirs()
140 | }.apply {
141 | MultiDexIO.writeDexFile(
142 | true,
143 | -1,
144 | this,
145 | BasicDexFileNamer(),
146 | object : DexFile {
147 | override fun getClasses() =
148 | this@BytecodePatchContext.classes.also(ProxyClassList::replaceClasses).toSet()
149 |
150 | override fun getOpcodes() = this@BytecodePatchContext.opcodes
151 | },
152 | DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
153 | ) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
154 | }.listFiles(FileFilter { it.isFile })!!.map {
155 | PatcherResult.PatchedDexFile(it.name, it.inputStream())
156 | }.toSet()
157 |
158 | System.gc()
159 |
160 | return patchedDexFileResults
161 | }
162 |
163 | /**
164 | * A lookup map for methods and the class they are a member of and classes.
165 | *
166 | * @param classes The list of classes to create the lookup maps from.
167 | */
168 | internal class LookupMaps internal constructor(classes: List) : Closeable {
169 | /**
170 | * Methods associated by strings referenced in it.
171 | */
172 | internal val methodsByStrings = MethodClassPairsLookupMap()
173 |
174 | // Lookup map for fast checking if a class exists by its type.
175 | val classesByType = mutableMapOf().apply {
176 | classes.forEach { classDef -> put(classDef.type, classDef) }
177 | }
178 |
179 | init {
180 | classes.forEach { classDef ->
181 | classDef.methods.forEach { method ->
182 | val methodClassPair: MethodClassPair = method to classDef
183 |
184 | // Add strings contained in the method as the key.
185 | method.instructionsOrNull?.forEach instructions@{ instruction ->
186 | if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
187 | return@instructions
188 | }
189 |
190 | val string = ((instruction as ReferenceInstruction).reference as StringReference).string
191 |
192 | methodsByStrings[string] = methodClassPair
193 | }
194 |
195 | // In the future, the class type could be added to the lookup map.
196 | // This would require MethodFingerprint to be changed to include the class type.
197 | }
198 | }
199 | }
200 |
201 | override fun close() {
202 | methodsByStrings.clear()
203 | classesByType.clear()
204 | }
205 | }
206 |
207 | override fun close() {
208 | lookupMaps.close()
209 | classes.clear()
210 | }
211 | }
212 |
213 | /**
214 | * A pair of a [Method] and the [ClassDef] it is a member of.
215 | */
216 | internal typealias MethodClassPair = Pair
217 |
218 | /**
219 | * A list of [MethodClassPair]s.
220 | */
221 | internal typealias MethodClassPairs = LinkedList
222 |
223 | /**
224 | * A lookup map for [MethodClassPairs]s.
225 | * The key is a string and the value is a list of [MethodClassPair]s.
226 | */
227 | internal class MethodClassPairsLookupMap : MutableMap by mutableMapOf() {
228 | /**
229 | * Add a [MethodClassPair] associated by any key.
230 | * If the key does not exist, a new list is created and the [MethodClassPair] is added to it.
231 | */
232 | internal operator fun set(key: String, methodClassPair: MethodClassPair) =
233 | apply { getOrPut(key) { MethodClassPairs() }.add(methodClassPair) }
234 | }
235 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/patch/PatchContext.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.patch
2 |
3 | import java.util.function.Supplier
4 |
5 | /**
6 | * A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
7 | */
8 |
9 | sealed interface PatchContext : Supplier
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.patch
2 |
3 | import app.revanced.patcher.InternalApi
4 | import app.revanced.patcher.PackageMetadata
5 | import app.revanced.patcher.PatcherConfig
6 | import app.revanced.patcher.PatcherResult
7 | import app.revanced.patcher.util.Document
8 | import brut.androlib.AaptInvoker
9 | import brut.androlib.ApkDecoder
10 | import brut.androlib.apk.UsesFramework
11 | import brut.androlib.res.Framework
12 | import brut.androlib.res.ResourcesDecoder
13 | import brut.androlib.res.decoder.AndroidManifestResourceParser
14 | import brut.androlib.res.decoder.XmlPullStreamDecoder
15 | import brut.androlib.res.xml.ResXmlPatcher
16 | import brut.directory.ExtFile
17 | import java.io.InputStream
18 | import java.io.OutputStream
19 | import java.nio.file.Files
20 | import java.util.logging.Logger
21 |
22 | /**
23 | * A context for patches containing the current state of resources.
24 | *
25 | * @param packageMetadata The [PackageMetadata] of the apk file.
26 | * @param config The [PatcherConfig] used to create this context.
27 | */
28 | class ResourcePatchContext internal constructor(
29 | private val packageMetadata: PackageMetadata,
30 | private val config: PatcherConfig,
31 | ) : PatchContext {
32 | private val logger = Logger.getLogger(ResourcePatchContext::class.java.name)
33 |
34 | /**
35 | * Read a document from an [InputStream].
36 | */
37 | fun document(inputStream: InputStream) = Document(inputStream)
38 |
39 | /**
40 | * Read and write documents in the [PatcherConfig.apkFiles].
41 | */
42 | fun document(path: String) = Document(get(path))
43 |
44 | /**
45 | * Set of resources from [PatcherConfig.apkFiles] to delete.
46 | */
47 | private val deleteResources = mutableSetOf()
48 |
49 | /**
50 | * Decode resources of [PatcherConfig.apkFile].
51 | *
52 | * @param mode The [ResourceMode] to use.
53 | */
54 | internal fun decodeResources(mode: ResourceMode) =
55 | with(packageMetadata.apkInfo) {
56 | config.initializeTemporaryFilesDirectories()
57 |
58 | // Needed to decode resources.
59 | val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this)
60 |
61 | if (mode == ResourceMode.FULL) {
62 | logger.info("Decoding resources")
63 |
64 | resourcesDecoder.decodeResources(config.apkFiles)
65 | resourcesDecoder.decodeManifest(config.apkFiles)
66 |
67 | // Needed to record uncompressed files.
68 | val apkDecoder = ApkDecoder(config.resourceConfig, this)
69 | apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
70 |
71 | usesFramework =
72 | UsesFramework().apply {
73 | ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
74 | }
75 | } else {
76 | logger.info("Decoding app manifest")
77 |
78 | // Decode manually instead of using resourceDecoder.decodeManifest
79 | // because it does not support decoding to an OutputStream.
80 | XmlPullStreamDecoder(
81 | AndroidManifestResourceParser(resourcesDecoder.resTable),
82 | resourcesDecoder.resXmlSerializer,
83 | ).decodeManifest(
84 | apkFile.directory.getFileInput("AndroidManifest.xml"),
85 | // Older Android versions do not support OutputStream.nullOutputStream()
86 | object : OutputStream() {
87 | override fun write(b: Int) { // Do nothing.
88 | }
89 | },
90 | )
91 |
92 | // Get the package name and version from the manifest using the XmlPullStreamDecoder.
93 | // XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
94 | packageMetadata.let { metadata ->
95 | metadata.packageName = resourcesDecoder.resTable.packageRenamed
96 | versionInfo.let {
97 | metadata.packageVersion = it.versionName ?: it.versionCode
98 | }
99 |
100 | /*
101 | The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
102 | because ResourcesDecoder.decodeResources loads the main package
103 | and not XmlPullStreamDecoder.decodeManifest.
104 | See ARSCDecoder.readTableType for more info.
105 |
106 | Set this to false again to prevent the ResTable from being flagged as sparse falsely.
107 | */
108 | metadata.apkInfo.sparseResources = false
109 | }
110 | }
111 | }
112 |
113 | /**
114 | * Compile resources in [PatcherConfig.apkFiles].
115 | *
116 | * @return The [PatcherResult.PatchedResources].
117 | */
118 | @InternalApi
119 | override fun get(): PatcherResult.PatchedResources? {
120 | if (config.resourceMode == ResourceMode.NONE) return null
121 |
122 | logger.info("Compiling modified resources")
123 |
124 | val resources = config.patchedFiles.resolve("resources").also { it.mkdirs() }
125 |
126 | val resourcesApkFile =
127 | if (config.resourceMode == ResourceMode.FULL) {
128 | resources.resolve("resources.apk").apply {
129 | // Compile the resources.apk file.
130 | AaptInvoker(
131 | config.resourceConfig,
132 | packageMetadata.apkInfo,
133 | ).invokeAapt(
134 | resources.resolve("resources.apk"),
135 | config.apkFiles.resolve("AndroidManifest.xml").also {
136 | ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
137 | },
138 | config.apkFiles.resolve("res"),
139 | null,
140 | null,
141 | packageMetadata.apkInfo.usesFramework.let { usesFramework ->
142 | usesFramework.ids.map { id ->
143 | Framework(config.resourceConfig).getFrameworkApk(id, usesFramework.tag)
144 | }.toTypedArray()
145 | },
146 | )
147 | }
148 | } else {
149 | null
150 | }
151 |
152 | val otherFiles =
153 | config.apkFiles.listFiles()!!.filter {
154 | // Excluded because present in resources.other.
155 | // TODO: We are reusing config.apkFiles as a temporarily directory for extracting resources.
156 | // This is not ideal as it could conflict with files such as the ones that we filter here.
157 | // The problem is that ResourcePatchContext#get returns a File relative to config.apkFiles,
158 | // and we need to extract files to that directory.
159 | // A solution would be to use config.apkFiles as the working directory for the patching process.
160 | // Once all patches have been executed, we can move the decoded resources to a new directory.
161 | // The filters wouldn't be needed anymore.
162 | // For now, we assume that the files we filter here are not needed for the patching process.
163 | it.name != "AndroidManifest.xml" &&
164 | it.name != "res" &&
165 | // Generated by Androlib.
166 | it.name != "build"
167 | }
168 |
169 | val otherResourceFiles =
170 | if (otherFiles.isNotEmpty()) {
171 | // Move the other resources files.
172 | resources.resolve("other").also { it.mkdirs() }.apply {
173 | otherFiles.forEach { file ->
174 | Files.move(file.toPath(), resolve(file.name).toPath())
175 | }
176 | }
177 | } else {
178 | null
179 | }
180 |
181 | return PatcherResult.PatchedResources(
182 | resourcesApkFile,
183 | otherResourceFiles,
184 | packageMetadata.apkInfo.doNotCompress?.toSet() ?: emptySet(),
185 | deleteResources,
186 | )
187 | }
188 |
189 | /**
190 | * Get a file from [PatcherConfig.apkFiles].
191 | *
192 | * @param path The path of the file.
193 | * @param copy Whether to copy the file from [PatcherConfig.apkFile] if it does not exist yet in [PatcherConfig.apkFiles].
194 | */
195 | operator fun get(
196 | path: String,
197 | copy: Boolean = true,
198 | ) = config.apkFiles.resolve(path).apply {
199 | if (copy && !exists()) {
200 | with(ExtFile(config.apkFile).directory) {
201 | if (containsFile(path) || containsDir(path)) {
202 | copyToDir(config.apkFiles, path)
203 | }
204 | }
205 | }
206 | }
207 |
208 | /**
209 | * Mark a file for deletion when the APK is rebuilt.
210 | *
211 | * @param name The name of the file to delete.
212 | */
213 | fun delete(name: String) = deleteResources.add(name)
214 |
215 | /**
216 | * How to handle resources decoding and compiling.
217 | */
218 | internal enum class ResourceMode {
219 | /**
220 | * Decode and compile all resources.
221 | */
222 | FULL,
223 |
224 | /**
225 | * Only extract resources from the APK.
226 | * The AndroidManifest.xml and resources inside /res are not decoded or compiled.
227 | */
228 | RAW_ONLY,
229 |
230 | /**
231 | * Do not decode or compile any resources.
232 | */
233 | NONE,
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util
2 |
3 | import app.revanced.patcher.patch.BytecodePatchContext
4 | import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
5 | import app.revanced.patcher.util.ClassMerger.Utils.filterAny
6 | import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
7 | import app.revanced.patcher.util.ClassMerger.Utils.isPublic
8 | import app.revanced.patcher.util.ClassMerger.Utils.toPublic
9 | import app.revanced.patcher.util.ClassMerger.Utils.traverseClassHierarchy
10 | import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
11 | import app.revanced.patcher.util.proxy.mutableTypes.MutableClass.Companion.toMutable
12 | import app.revanced.patcher.util.proxy.mutableTypes.MutableField
13 | import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
14 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
15 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
16 | import com.android.tools.smali.dexlib2.AccessFlags
17 | import com.android.tools.smali.dexlib2.iface.ClassDef
18 | import com.android.tools.smali.dexlib2.util.MethodUtil
19 | import java.util.logging.Logger
20 | import kotlin.reflect.KFunction2
21 |
22 | /**
23 | * Experimental class to merge a [ClassDef] with another.
24 | * Note: This will not consider method implementations or if the class is missing a superclass or interfaces.
25 | */
26 | internal object ClassMerger {
27 | private val logger = Logger.getLogger(ClassMerger::class.java.name)
28 |
29 | /**
30 | * Merge a class with [otherClass].
31 | *
32 | * @param otherClass The class to merge with
33 | * @param context The context to traverse the class hierarchy in.
34 | * @return The merged class or the original class if no merge was needed.
35 | */
36 | fun ClassDef.merge(
37 | otherClass: ClassDef,
38 | context: BytecodePatchContext,
39 | ) = this
40 | // .fixFieldAccess(otherClass)
41 | // .fixMethodAccess(otherClass)
42 | .addMissingFields(otherClass)
43 | .addMissingMethods(otherClass)
44 | .publicize(otherClass, context)
45 |
46 | /**
47 | * Add methods which are missing but existing in [fromClass].
48 | *
49 | * @param fromClass The class to add missing methods from.
50 | */
51 | private fun ClassDef.addMissingMethods(fromClass: ClassDef): ClassDef {
52 | val missingMethods =
53 | fromClass.methods.let { fromMethods ->
54 | methods.filterNot { method ->
55 | fromMethods.any { fromMethod ->
56 | MethodUtil.methodSignaturesMatch(fromMethod, method)
57 | }
58 | }
59 | }
60 |
61 | if (missingMethods.isEmpty()) return this
62 |
63 | logger.fine { "Found ${missingMethods.size} missing methods" }
64 |
65 | return asMutableClass().apply {
66 | methods.addAll(missingMethods.map { it.toMutable() })
67 | }
68 | }
69 |
70 | /**
71 | * Add fields which are missing but existing in [fromClass].
72 | *
73 | * @param fromClass The class to add missing fields from.
74 | */
75 | private fun ClassDef.addMissingFields(fromClass: ClassDef): ClassDef {
76 | val missingFields =
77 | fields.filterNotAny(fromClass.fields) { field, fromField ->
78 | fromField.name == field.name
79 | }
80 |
81 | if (missingFields.isEmpty()) return this
82 |
83 | logger.fine { "Found ${missingFields.size} missing fields" }
84 |
85 | return asMutableClass().apply {
86 | fields.addAll(missingFields.map { it.toMutable() })
87 | }
88 | }
89 |
90 | /**
91 | * Make a class and its super class public recursively.
92 | * @param reference The class to check the [AccessFlags] of.
93 | * @param context The context to traverse the class hierarchy in.
94 | */
95 | private fun ClassDef.publicize(
96 | reference: ClassDef,
97 | context: BytecodePatchContext,
98 | ) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) {
99 | this.asMutableClass().apply {
100 | context.traverseClassHierarchy(this) {
101 | if (accessFlags.isPublic()) return@traverseClassHierarchy
102 |
103 | logger.fine { "Publicizing ${this.type}" }
104 |
105 | accessFlags = accessFlags.toPublic()
106 | }
107 | }
108 | } else {
109 | this
110 | }
111 |
112 | /**
113 | * Publicize fields if they are public in [reference].
114 | *
115 | * @param reference The class to check the [AccessFlags] of the fields in.
116 | */
117 | private fun ClassDef.fixFieldAccess(reference: ClassDef): ClassDef {
118 | val brokenFields =
119 | fields.filterAny(reference.fields) { field, referenceField ->
120 | if (field.name != referenceField.name) return@filterAny false
121 |
122 | referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic()
123 | }
124 |
125 | if (brokenFields.isEmpty()) return this
126 |
127 | logger.fine { "Found ${brokenFields.size} broken fields" }
128 |
129 | /**
130 | * Make a field public.
131 | */
132 | fun MutableField.publicize() {
133 | accessFlags = accessFlags.toPublic()
134 | }
135 |
136 | return asMutableClass().apply {
137 | fields.filter { brokenFields.contains(it) }.forEach(MutableField::publicize)
138 | }
139 | }
140 |
141 | /**
142 | * Publicize methods if they are public in [reference].
143 | *
144 | * @param reference The class to check the [AccessFlags] of the methods in.
145 | */
146 | private fun ClassDef.fixMethodAccess(reference: ClassDef): ClassDef {
147 | val brokenMethods =
148 | methods.filterAny(reference.methods) { method, referenceMethod ->
149 | if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false
150 |
151 | referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic()
152 | }
153 |
154 | if (brokenMethods.isEmpty()) return this
155 |
156 | logger.fine { "Found ${brokenMethods.size} methods" }
157 |
158 | /**
159 | * Make a method public.
160 | */
161 | fun MutableMethod.publicize() {
162 | accessFlags = accessFlags.toPublic()
163 | }
164 |
165 | return asMutableClass().apply {
166 | methods.filter { brokenMethods.contains(it) }.forEach(MutableMethod::publicize)
167 | }
168 | }
169 |
170 | private object Utils {
171 | /**
172 | * traverse the class hierarchy starting from the given root class
173 | *
174 | * @param targetClass the class to start traversing the class hierarchy from
175 | * @param callback function that is called for every class in the hierarchy
176 | */
177 | fun BytecodePatchContext.traverseClassHierarchy(
178 | targetClass: MutableClass,
179 | callback: MutableClass.() -> Unit,
180 | ) {
181 | callback(targetClass)
182 |
183 | targetClass.superclass ?: return
184 | this.classBy { targetClass.superclass == it.type }?.mutableClass?.let {
185 | traverseClassHierarchy(it, callback)
186 | }
187 | }
188 |
189 | fun ClassDef.asMutableClass() = if (this is MutableClass) this else this.toMutable()
190 |
191 | /**
192 | * Check if the [AccessFlags.PUBLIC] flag is set.
193 | *
194 | * @return True, if the flag is set.
195 | */
196 | fun Int.isPublic() = AccessFlags.PUBLIC.isSet(this)
197 |
198 | /**
199 | * Make [AccessFlags] public.
200 | *
201 | * @return The new [AccessFlags].
202 | */
203 | fun Int.toPublic() = or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
204 |
205 | /**
206 | * Filter [this] on [needles] matching the given [predicate].
207 | *
208 | * @param needles The needles to filter [this] with.
209 | * @param predicate The filter.
210 | * @return The [this] filtered on [needles] matching the given [predicate].
211 | */
212 | fun Iterable.filterAny(
213 | needles: Iterable,
214 | predicate: (HayType, NeedleType) -> Boolean,
215 | ) = Iterable::filter.any(this, needles, predicate)
216 |
217 | /**
218 | * Filter [this] on [needles] not matching the given [predicate].
219 | *
220 | * @param needles The needles to filter [this] with.
221 | * @param predicate The filter.
222 | * @return The [this] filtered on [needles] not matching the given [predicate].
223 | */
224 | fun Iterable.filterNotAny(
225 | needles: Iterable,
226 | predicate: (HayType, NeedleType) -> Boolean,
227 | ) = Iterable::filterNot.any(this, needles, predicate)
228 |
229 | fun KFunction2, (HayType) -> Boolean, List>.any(
230 | haystack: Iterable,
231 | needles: Iterable,
232 | predicate: (HayType, NeedleType) -> Boolean,
233 | ) = this(haystack) { hay ->
234 | needles.any { needle ->
235 | predicate(hay, needle)
236 | }
237 | }
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/Document.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util
2 |
3 | import org.w3c.dom.Document
4 | import java.io.Closeable
5 | import java.io.File
6 | import java.io.InputStream
7 | import javax.xml.parsers.DocumentBuilderFactory
8 | import javax.xml.transform.TransformerFactory
9 | import javax.xml.transform.dom.DOMSource
10 | import javax.xml.transform.stream.StreamResult
11 |
12 | class Document internal constructor(
13 | inputStream: InputStream,
14 | ) : Document by DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream), Closeable {
15 | private var file: File? = null
16 |
17 | init {
18 | normalize()
19 | }
20 |
21 | internal constructor(file: File) : this(file.inputStream()) {
22 | this.file = file
23 | readerCount.merge(file, 1, Int::plus)
24 | }
25 |
26 | override fun close() {
27 | file?.let {
28 | if (readerCount[it]!! > 1) {
29 | throw IllegalStateException(
30 | "Two or more instances are currently reading $it." +
31 | "To be able to close this instance, no other instances may be reading $it at the same time.",
32 | )
33 | } else {
34 | readerCount.remove(it)
35 | }
36 |
37 | it.outputStream().use { stream ->
38 | TransformerFactory.newInstance()
39 | .newTransformer()
40 | .transform(DOMSource(this), StreamResult(stream))
41 | }
42 | }
43 | }
44 |
45 | private companion object {
46 | private val readerCount = mutableMapOf()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("unused")
2 |
3 | package app.revanced.patcher.util
4 |
5 | import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
6 | import app.revanced.patcher.patch.BytecodePatchContext
7 | import app.revanced.patcher.util.MethodNavigator.NavigateException
8 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
9 | import com.android.tools.smali.dexlib2.iface.ClassDef
10 | import com.android.tools.smali.dexlib2.iface.Method
11 | import com.android.tools.smali.dexlib2.iface.instruction.Instruction
12 | import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
13 | import com.android.tools.smali.dexlib2.iface.reference.MethodReference
14 | import com.android.tools.smali.dexlib2.util.MethodUtil
15 | import kotlin.reflect.KProperty
16 |
17 | /**
18 | * A navigator for methods.
19 | *
20 | * @param startMethod The [Method] to start navigating from.
21 | *
22 | * @constructor Creates a new [MethodNavigator].
23 | *
24 | * @throws NavigateException If the method does not have an implementation.
25 | * @throws NavigateException If the instruction at the specified index is not a method reference.
26 | */
27 | context(BytecodePatchContext)
28 | class MethodNavigator internal constructor(
29 | private var startMethod: MethodReference,
30 | ) {
31 | private var lastNavigatedMethodReference = startMethod
32 |
33 | private val lastNavigatedMethodInstructions
34 | get() = with(original()) {
35 | instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
36 | }
37 |
38 | /**
39 | * Navigate to the method at the specified index.
40 | *
41 | * @param index The index of the method to navigate to.
42 | *
43 | * @return This [MethodNavigator].
44 | */
45 | fun to(vararg index: Int): MethodNavigator {
46 | index.forEach {
47 | lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
48 | }
49 |
50 | return this
51 | }
52 |
53 | /**
54 | * Navigate to the method at the specified index that matches the specified predicate.
55 | *
56 | * @param index The index of the method to navigate to.
57 | * @param predicate The predicate to match.
58 | */
59 | fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
60 | lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
61 | .filter(predicate).asIterable().getMethodReferenceAt(index)
62 |
63 | return this
64 | }
65 |
66 | /**
67 | * Get the method reference at the specified index.
68 | *
69 | * @param index The index of the method reference to get.
70 | */
71 | private fun Iterable.getMethodReferenceAt(index: Int): MethodReference {
72 | val instruction = elementAt(index) as? ReferenceInstruction
73 | ?: throw NavigateException("Instruction at index $index is not a method reference.")
74 |
75 | return instruction.reference as MethodReference
76 | }
77 |
78 | /**
79 | * Get the last navigated method mutably.
80 | *
81 | * @return The last navigated method mutably.
82 | */
83 | fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
84 | as MutableMethod
85 |
86 | /**
87 | * Get the last navigated method mutably.
88 | *
89 | * @return The last navigated method mutably.
90 | */
91 | operator fun getValue(nothing: Nothing?, property: KProperty<*>) = stop()
92 |
93 | /**
94 | * Get the last navigated method immutably.
95 | *
96 | * @return The last navigated method immutably.
97 | */
98 | fun original(): Method = classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
99 |
100 | /**
101 | * Predicate to match the class defining the current method reference.
102 | */
103 | private val matchesCurrentMethodReferenceDefiningClass = { classDef: ClassDef ->
104 | classDef.type == lastNavigatedMethodReference.definingClass
105 | }
106 |
107 | /**
108 | * Find the first [lastNavigatedMethodReference] in the class.
109 | */
110 | private val ClassDef.firstMethodBySignature
111 | get() = methods.first {
112 | MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
113 | }
114 |
115 | /**
116 | * An exception thrown when navigating fails.
117 | *
118 | * @param message The message of the exception.
119 | */
120 | internal class NavigateException internal constructor(message: String) : Exception(message)
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util
2 |
3 | import app.revanced.patcher.util.proxy.ClassProxy
4 | import com.android.tools.smali.dexlib2.iface.ClassDef
5 |
6 | /**
7 | * A list of classes and proxies.
8 | *
9 | * @param classes The classes to be backed by proxies.
10 | */
11 | class ProxyClassList internal constructor(classes: MutableList) : MutableList by classes {
12 | internal val proxyPool = mutableListOf()
13 |
14 | /**
15 | * Replace all classes with their mutated versions.
16 | */
17 | internal fun replaceClasses() =
18 | proxyPool.removeIf { proxy ->
19 | // If the proxy is unused, return false to keep it in the proxies list.
20 | if (!proxy.resolved) return@removeIf false
21 |
22 | // If it has been used, replace the original class with the mutable class.
23 | remove(proxy.immutableClass)
24 | add(proxy.mutableClass)
25 |
26 | // Return true to remove the proxy from the proxies list.
27 | return@removeIf true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
4 | import com.android.tools.smali.dexlib2.iface.ClassDef
5 |
6 | /**
7 | * A proxy class for a [ClassDef].
8 | *
9 | * A class proxy simply holds a reference to the original class
10 | * and allocates a mutable clone for the original class if needed.
11 | *
12 | * @param immutableClass The class to proxy.
13 | */
14 | class ClassProxy internal constructor(
15 | val immutableClass: ClassDef,
16 | ) {
17 | /**
18 | * Weather the proxy was actually used.
19 | */
20 | internal var resolved = false
21 |
22 | /**
23 | * The mutable clone of the original class.
24 | *
25 | * Note: This is only allocated if the proxy is actually used.
26 | */
27 | val mutableClass by lazy {
28 | resolved = true
29 | if (immutableClass is MutableClass) {
30 | immutableClass
31 | } else {
32 | MutableClass(immutableClass)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable
4 | import com.android.tools.smali.dexlib2.base.BaseAnnotation
5 | import com.android.tools.smali.dexlib2.iface.Annotation
6 |
7 | class MutableAnnotation(annotation: Annotation) : BaseAnnotation() {
8 | private val visibility = annotation.visibility
9 | private val type = annotation.type
10 | private val _elements by lazy { annotation.elements.map { element -> element.toMutable() }.toMutableSet() }
11 |
12 | override fun getType(): String = type
13 |
14 | override fun getElements(): MutableSet = _elements
15 |
16 | override fun getVisibility(): Int = visibility
17 |
18 | companion object {
19 | fun Annotation.toMutable(): MutableAnnotation = MutableAnnotation(this)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue
4 | import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
5 | import com.android.tools.smali.dexlib2.base.BaseAnnotationElement
6 | import com.android.tools.smali.dexlib2.iface.AnnotationElement
7 | import com.android.tools.smali.dexlib2.iface.value.EncodedValue
8 |
9 | class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnotationElement() {
10 | private var name = annotationElement.name
11 | private var value = annotationElement.value.toMutable()
12 |
13 | fun setName(name: String) {
14 | this.name = name
15 | }
16 |
17 | fun setValue(value: MutableEncodedValue) {
18 | this.value = value
19 | }
20 |
21 | override fun getName(): String = name
22 |
23 | override fun getValue(): EncodedValue = value
24 |
25 | companion object {
26 | fun AnnotationElement.toMutable(): MutableAnnotationElement = MutableAnnotationElement(this)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
4 | import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
5 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
6 | import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference
7 | import com.android.tools.smali.dexlib2.iface.ClassDef
8 | import com.android.tools.smali.dexlib2.util.FieldUtil
9 | import com.android.tools.smali.dexlib2.util.MethodUtil
10 |
11 | class MutableClass(classDef: ClassDef) :
12 | BaseTypeReference(),
13 | ClassDef {
14 | // Class
15 | private var type = classDef.type
16 | private var sourceFile = classDef.sourceFile
17 | private var accessFlags = classDef.accessFlags
18 | private var superclass = classDef.superclass
19 |
20 | private val _interfaces by lazy { classDef.interfaces.toMutableList() }
21 | private val _annotations by lazy {
22 | classDef.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
23 | }
24 |
25 | // Methods
26 | private val _methods by lazy { classDef.methods.map { method -> method.toMutable() }.toMutableSet() }
27 | private val _directMethods by lazy { _methods.filter { method -> MethodUtil.isDirect(method) }.toMutableSet() }
28 | private val _virtualMethods by lazy { _methods.filter { method -> !MethodUtil.isDirect(method) }.toMutableSet() }
29 |
30 | // Fields
31 | private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() }
32 | private val _staticFields by lazy { _fields.filter { field -> FieldUtil.isStatic(field) }.toMutableSet() }
33 | private val _instanceFields by lazy { _fields.filter { field -> !FieldUtil.isStatic(field) }.toMutableSet() }
34 |
35 | fun setType(type: String) {
36 | this.type = type
37 | }
38 |
39 | fun setSourceFile(sourceFile: String?) {
40 | this.sourceFile = sourceFile
41 | }
42 |
43 | fun setAccessFlags(accessFlags: Int) {
44 | this.accessFlags = accessFlags
45 | }
46 |
47 | fun setSuperClass(superclass: String?) {
48 | this.superclass = superclass
49 | }
50 |
51 | override fun getType(): String = type
52 |
53 | override fun getAccessFlags(): Int = accessFlags
54 |
55 | override fun getSourceFile(): String? = sourceFile
56 |
57 | override fun getSuperclass(): String? = superclass
58 |
59 | override fun getInterfaces(): MutableList = _interfaces
60 |
61 | override fun getAnnotations(): MutableSet = _annotations
62 |
63 | override fun getStaticFields(): MutableSet = _staticFields
64 |
65 | override fun getInstanceFields(): MutableSet = _instanceFields
66 |
67 | override fun getFields(): MutableSet = _fields
68 |
69 | override fun getDirectMethods(): MutableSet = _directMethods
70 |
71 | override fun getVirtualMethods(): MutableSet = _virtualMethods
72 |
73 | override fun getMethods(): MutableSet = _methods
74 |
75 | companion object {
76 | fun ClassDef.toMutable(): MutableClass = MutableClass(this)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
4 | import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue
5 | import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
6 | import com.android.tools.smali.dexlib2.HiddenApiRestriction
7 | import com.android.tools.smali.dexlib2.base.reference.BaseFieldReference
8 | import com.android.tools.smali.dexlib2.iface.Field
9 |
10 | class MutableField(field: Field) :
11 | BaseFieldReference(),
12 | Field {
13 | private var definingClass = field.definingClass
14 | private var name = field.name
15 | private var type = field.type
16 | private var accessFlags = field.accessFlags
17 |
18 | private var initialValue = field.initialValue?.toMutable()
19 | private val _annotations by lazy { field.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
20 | private val _hiddenApiRestrictions by lazy { field.hiddenApiRestrictions }
21 |
22 | fun setDefiningClass(definingClass: String) {
23 | this.definingClass = definingClass
24 | }
25 |
26 | fun setName(name: String) {
27 | this.name = name
28 | }
29 |
30 | fun setType(type: String) {
31 | this.type = type
32 | }
33 |
34 | fun setAccessFlags(accessFlags: Int) {
35 | this.accessFlags = accessFlags
36 | }
37 |
38 | fun setInitialValue(initialValue: MutableEncodedValue?) {
39 | this.initialValue = initialValue
40 | }
41 |
42 | override fun getDefiningClass(): String = this.definingClass
43 |
44 | override fun getName(): String = this.name
45 |
46 | override fun getType(): String = this.type
47 |
48 | override fun getAnnotations(): MutableSet = this._annotations
49 |
50 | override fun getAccessFlags(): Int = this.accessFlags
51 |
52 | override fun getHiddenApiRestrictions(): MutableSet = this._hiddenApiRestrictions
53 |
54 | override fun getInitialValue(): MutableEncodedValue? = this.initialValue
55 |
56 | companion object {
57 | fun Field.toMutable(): MutableField = MutableField(this)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
4 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethodParameter.Companion.toMutable
5 | import com.android.tools.smali.dexlib2.HiddenApiRestriction
6 | import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference
7 | import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
8 | import com.android.tools.smali.dexlib2.iface.Method
9 |
10 | class MutableMethod(method: Method) :
11 | BaseMethodReference(),
12 | Method {
13 | private var definingClass = method.definingClass
14 | private var name = method.name
15 | private var accessFlags = method.accessFlags
16 | private var returnType = method.returnType
17 |
18 | // Create own mutable MethodImplementation (due to not being able to change members like register count)
19 | private val _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } }
20 | private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
21 | private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
22 | private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
23 | private val _hiddenApiRestrictions by lazy { method.hiddenApiRestrictions }
24 |
25 | fun setDefiningClass(definingClass: String) {
26 | this.definingClass = definingClass
27 | }
28 |
29 | fun setName(name: String) {
30 | this.name = name
31 | }
32 |
33 | fun setAccessFlags(accessFlags: Int) {
34 | this.accessFlags = accessFlags
35 | }
36 |
37 | fun setReturnType(returnType: String) {
38 | this.returnType = returnType
39 | }
40 |
41 | override fun getDefiningClass(): String = definingClass
42 |
43 | override fun getName(): String = name
44 |
45 | override fun getParameterTypes(): MutableList = _parameterTypes
46 |
47 | override fun getReturnType(): String = returnType
48 |
49 | override fun getAnnotations(): MutableSet = _annotations
50 |
51 | override fun getAccessFlags(): Int = accessFlags
52 |
53 | override fun getHiddenApiRestrictions(): MutableSet = _hiddenApiRestrictions
54 |
55 | override fun getParameters(): MutableList = _parameters
56 |
57 | override fun getImplementation(): MutableMethodImplementation? = _implementation
58 |
59 | companion object {
60 | fun Method.toMutable(): MutableMethod = MutableMethod(this)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.toMutable
4 | import com.android.tools.smali.dexlib2.base.BaseMethodParameter
5 | import com.android.tools.smali.dexlib2.iface.MethodParameter
6 |
7 | // TODO: finish overriding all members if necessary
8 | class MutableMethodParameter(parameter: MethodParameter) :
9 | BaseMethodParameter(),
10 | MethodParameter {
11 | private var type = parameter.type
12 | private var name = parameter.name
13 | private var signature = parameter.signature
14 | private val _annotations by lazy {
15 | parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
16 | }
17 |
18 | override fun getType(): String = type
19 |
20 | override fun getName(): String? = name
21 |
22 | override fun getSignature(): String? = signature
23 |
24 | override fun getAnnotations(): MutableSet = _annotations
25 |
26 | companion object {
27 | fun MethodParameter.toMutable(): MutableMethodParameter = MutableMethodParameter(this)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable
4 | import com.android.tools.smali.dexlib2.base.value.BaseAnnotationEncodedValue
5 | import com.android.tools.smali.dexlib2.iface.AnnotationElement
6 | import com.android.tools.smali.dexlib2.iface.value.AnnotationEncodedValue
7 |
8 | class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) :
9 | BaseAnnotationEncodedValue(),
10 | MutableEncodedValue {
11 | private var type = annotationEncodedValue.type
12 |
13 | private val _elements by lazy {
14 | annotationEncodedValue.elements.map { annotationElement -> annotationElement.toMutable() }.toMutableSet()
15 | }
16 |
17 | override fun getType(): String = this.type
18 |
19 | fun setType(type: String) {
20 | this.type = type
21 | }
22 |
23 | override fun getElements(): MutableSet = _elements
24 |
25 | companion object {
26 | fun AnnotationEncodedValue.toMutable(): MutableAnnotationEncodedValue = MutableAnnotationEncodedValue(this)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable
4 | import com.android.tools.smali.dexlib2.base.value.BaseArrayEncodedValue
5 | import com.android.tools.smali.dexlib2.iface.value.ArrayEncodedValue
6 | import com.android.tools.smali.dexlib2.iface.value.EncodedValue
7 |
8 | class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) :
9 | BaseArrayEncodedValue(),
10 | MutableEncodedValue {
11 | private val _value by lazy {
12 | arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList()
13 | }
14 |
15 | override fun getValue(): MutableList = _value
16 |
17 | companion object {
18 | fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue = MutableArrayEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.BooleanEncodedValue
5 |
6 | class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) :
7 | BaseBooleanEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = booleanEncodedValue.value
10 |
11 | override fun getValue(): Boolean = this.value
12 |
13 | fun setValue(value: Boolean) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun BooleanEncodedValue.toMutable(): MutableBooleanEncodedValue = MutableBooleanEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseByteEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
5 |
6 | class MutableByteEncodedValue(byteEncodedValue: ByteEncodedValue) :
7 | BaseByteEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = byteEncodedValue.value
10 |
11 | override fun getValue(): Byte = this.value
12 |
13 | fun setValue(value: Byte) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseCharEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.CharEncodedValue
5 |
6 | class MutableCharEncodedValue(charEncodedValue: CharEncodedValue) :
7 | BaseCharEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = charEncodedValue.value
10 |
11 | override fun getValue(): Char = this.value
12 |
13 | fun setValue(value: Char) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun CharEncodedValue.toMutable(): MutableCharEncodedValue = MutableCharEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.DoubleEncodedValue
5 |
6 | class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) :
7 | BaseDoubleEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = doubleEncodedValue.value
10 |
11 | override fun getValue(): Double = this.value
12 |
13 | fun setValue(value: Double) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun DoubleEncodedValue.toMutable(): MutableDoubleEncodedValue = MutableDoubleEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.ValueType
4 | import com.android.tools.smali.dexlib2.iface.value.*
5 |
6 | interface MutableEncodedValue : EncodedValue {
7 | companion object {
8 | fun EncodedValue.toMutable(): MutableEncodedValue {
9 | return when (this.valueType) {
10 | ValueType.TYPE -> MutableTypeEncodedValue(this as TypeEncodedValue)
11 | ValueType.FIELD -> MutableFieldEncodedValue(this as FieldEncodedValue)
12 | ValueType.METHOD -> MutableMethodEncodedValue(this as MethodEncodedValue)
13 | ValueType.ENUM -> MutableEnumEncodedValue(this as EnumEncodedValue)
14 | ValueType.ARRAY -> MutableArrayEncodedValue(this as ArrayEncodedValue)
15 | ValueType.ANNOTATION -> MutableAnnotationEncodedValue(this as AnnotationEncodedValue)
16 | ValueType.BYTE -> MutableByteEncodedValue(this as ByteEncodedValue)
17 | ValueType.SHORT -> MutableShortEncodedValue(this as ShortEncodedValue)
18 | ValueType.CHAR -> MutableCharEncodedValue(this as CharEncodedValue)
19 | ValueType.INT -> MutableIntEncodedValue(this as IntEncodedValue)
20 | ValueType.LONG -> MutableLongEncodedValue(this as LongEncodedValue)
21 | ValueType.FLOAT -> MutableFloatEncodedValue(this as FloatEncodedValue)
22 | ValueType.DOUBLE -> MutableDoubleEncodedValue(this as DoubleEncodedValue)
23 | ValueType.METHOD_TYPE -> MutableMethodTypeEncodedValue(this as MethodTypeEncodedValue)
24 | ValueType.METHOD_HANDLE -> MutableMethodHandleEncodedValue(this as MethodHandleEncodedValue)
25 | ValueType.STRING -> MutableStringEncodedValue(this as StringEncodedValue)
26 | ValueType.BOOLEAN -> MutableBooleanEncodedValue(this as BooleanEncodedValue)
27 | ValueType.NULL -> MutableNullEncodedValue()
28 | else -> this as MutableEncodedValue
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseEnumEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.reference.FieldReference
5 | import com.android.tools.smali.dexlib2.iface.value.EnumEncodedValue
6 |
7 | class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) :
8 | BaseEnumEncodedValue(),
9 | MutableEncodedValue {
10 | private var value = enumEncodedValue.value
11 |
12 | override fun getValue(): FieldReference = this.value
13 |
14 | fun setValue(value: FieldReference) {
15 | this.value = value
16 | }
17 |
18 | companion object {
19 | fun EnumEncodedValue.toMutable(): MutableEnumEncodedValue = MutableEnumEncodedValue(this)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.ValueType
4 | import com.android.tools.smali.dexlib2.base.value.BaseFieldEncodedValue
5 | import com.android.tools.smali.dexlib2.iface.reference.FieldReference
6 | import com.android.tools.smali.dexlib2.iface.value.FieldEncodedValue
7 |
8 | class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) :
9 | BaseFieldEncodedValue(),
10 | MutableEncodedValue {
11 | private var value = fieldEncodedValue.value
12 |
13 | override fun getValueType(): Int = ValueType.FIELD
14 |
15 | override fun getValue(): FieldReference = this.value
16 |
17 | fun setValue(value: FieldReference) {
18 | this.value = value
19 | }
20 |
21 | companion object {
22 | fun FieldEncodedValue.toMutable(): MutableFieldEncodedValue = MutableFieldEncodedValue(this)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseFloatEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.FloatEncodedValue
5 |
6 | class MutableFloatEncodedValue(floatEncodedValue: FloatEncodedValue) :
7 | BaseFloatEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = floatEncodedValue.value
10 |
11 | override fun getValue(): Float = this.value
12 |
13 | fun setValue(value: Float) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue = MutableFloatEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseIntEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.IntEncodedValue
5 |
6 | class MutableIntEncodedValue(intEncodedValue: IntEncodedValue) :
7 | BaseIntEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = intEncodedValue.value
10 |
11 | override fun getValue(): Int = this.value
12 |
13 | fun setValue(value: Int) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun IntEncodedValue.toMutable(): MutableIntEncodedValue = MutableIntEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseLongEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.LongEncodedValue
5 |
6 | class MutableLongEncodedValue(longEncodedValue: LongEncodedValue) :
7 | BaseLongEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = longEncodedValue.value
10 |
11 | override fun getValue(): Long = this.value
12 |
13 | fun setValue(value: Long) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun LongEncodedValue.toMutable(): MutableLongEncodedValue = MutableLongEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseMethodEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.reference.MethodReference
5 | import com.android.tools.smali.dexlib2.iface.value.MethodEncodedValue
6 |
7 | class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) :
8 | BaseMethodEncodedValue(),
9 | MutableEncodedValue {
10 | private var value = methodEncodedValue.value
11 |
12 | override fun getValue(): MethodReference = this.value
13 |
14 | fun setValue(value: MethodReference) {
15 | this.value = value
16 | }
17 |
18 | companion object {
19 | fun MethodEncodedValue.toMutable(): MutableMethodEncodedValue = MutableMethodEncodedValue(this)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseMethodHandleEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.reference.MethodHandleReference
5 | import com.android.tools.smali.dexlib2.iface.value.MethodHandleEncodedValue
6 |
7 | class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEncodedValue) :
8 | BaseMethodHandleEncodedValue(),
9 | MutableEncodedValue {
10 | private var value = methodHandleEncodedValue.value
11 |
12 | override fun getValue(): MethodHandleReference = this.value
13 |
14 | fun setValue(value: MethodHandleReference) {
15 | this.value = value
16 | }
17 |
18 | companion object {
19 | fun MethodHandleEncodedValue.toMutable(): MutableMethodHandleEncodedValue = MutableMethodHandleEncodedValue(this)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseMethodTypeEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference
5 | import com.android.tools.smali.dexlib2.iface.value.MethodTypeEncodedValue
6 |
7 | class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) :
8 | BaseMethodTypeEncodedValue(),
9 | MutableEncodedValue {
10 | private var value = methodTypeEncodedValue.value
11 |
12 | override fun getValue(): MethodProtoReference = this.value
13 |
14 | fun setValue(value: MethodProtoReference) {
15 | this.value = value
16 | }
17 |
18 | companion object {
19 | fun MethodTypeEncodedValue.toMutable(): MutableMethodTypeEncodedValue = MutableMethodTypeEncodedValue(this)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseNullEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
5 |
6 | class MutableNullEncodedValue :
7 | BaseNullEncodedValue(),
8 | MutableEncodedValue {
9 | companion object {
10 | fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseShortEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.ShortEncodedValue
5 |
6 | class MutableShortEncodedValue(shortEncodedValue: ShortEncodedValue) :
7 | BaseShortEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = shortEncodedValue.value
10 |
11 | override fun getValue(): Short = this.value
12 |
13 | fun setValue(value: Short) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun ShortEncodedValue.toMutable(): MutableShortEncodedValue = MutableShortEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseStringEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
5 | import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue
6 |
7 | class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) :
8 | BaseStringEncodedValue(),
9 | MutableEncodedValue {
10 | private var value = stringEncodedValue.value
11 |
12 | override fun getValue(): String = this.value
13 |
14 | fun setValue(value: String) {
15 | this.value = value
16 | }
17 |
18 | companion object {
19 | fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
2 |
3 | import com.android.tools.smali.dexlib2.base.value.BaseTypeEncodedValue
4 | import com.android.tools.smali.dexlib2.iface.value.TypeEncodedValue
5 |
6 | class MutableTypeEncodedValue(typeEncodedValue: TypeEncodedValue) :
7 | BaseTypeEncodedValue(),
8 | MutableEncodedValue {
9 | private var value = typeEncodedValue.value
10 |
11 | override fun getValue(): String = this.value
12 |
13 | fun setValue(value: String) {
14 | this.value = value
15 | }
16 |
17 | companion object {
18 | fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue = MutableTypeEncodedValue(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.smali
2 |
3 | import com.android.tools.smali.dexlib2.iface.instruction.Instruction
4 |
5 | /**
6 | * A class that represents a label for an instruction.
7 | * @param name The label name.
8 | * @param instruction The instruction that this label is for.
9 | */
10 | data class ExternalLabel(internal val name: String, internal val instruction: Instruction)
11 |
--------------------------------------------------------------------------------
/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.smali
2 |
3 | import app.revanced.patcher.extensions.InstructionExtensions.instructions
4 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
5 | import com.android.tools.smali.dexlib2.AccessFlags
6 | import com.android.tools.smali.dexlib2.Opcodes
7 | import com.android.tools.smali.dexlib2.builder.BuilderInstruction
8 | import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
9 | import com.android.tools.smali.smali.LexerErrorInterface
10 | import com.android.tools.smali.smali.smaliFlexLexer
11 | import com.android.tools.smali.smali.smaliParser
12 | import com.android.tools.smali.smali.smaliTreeWalker
13 | import org.antlr.runtime.CommonTokenStream
14 | import org.antlr.runtime.TokenSource
15 | import org.antlr.runtime.tree.CommonTreeNodeStream
16 | import java.io.InputStreamReader
17 | import java.util.*
18 |
19 | private const val METHOD_TEMPLATE = """
20 | .class LInlineCompiler;
21 | .super Ljava/lang/Object;
22 | .method %s dummyMethod(%s)V
23 | .registers %d
24 | %s
25 | .end method
26 | """
27 |
28 | class InlineSmaliCompiler {
29 | companion object {
30 | /**
31 | * Compiles a string of Smali code to a list of instructions.
32 | * Special registers (such as p0, p1) will only work correctly
33 | * if the parameters and registers of the method are passed.
34 | */
35 | fun compile(
36 | instructions: String,
37 | parameters: String,
38 | registers: Int,
39 | forStaticMethod: Boolean,
40 | ): List {
41 | val input =
42 | METHOD_TEMPLATE.format(
43 | Locale.ENGLISH,
44 | if (forStaticMethod) {
45 | "static"
46 | } else {
47 | ""
48 | },
49 | parameters,
50 | registers,
51 | instructions,
52 | )
53 | val reader = InputStreamReader(input.byteInputStream())
54 | val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
55 | val tokens = CommonTokenStream(lexer as TokenSource)
56 | val parser = smaliParser(tokens)
57 | val result = parser.smali_file()
58 | if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) {
59 | throw IllegalStateException(
60 | "Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!",
61 | )
62 | }
63 | val treeStream = CommonTreeNodeStream(result.tree)
64 | treeStream.tokenStream = tokens
65 | val dexGen = smaliTreeWalker(treeStream)
66 | dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault()))
67 | val classDef = dexGen.smali_file()
68 | return classDef.methods.first().instructions.map { it as BuilderInstruction }
69 | }
70 | }
71 | }
72 |
73 | /**
74 | * Compile lines of Smali code to a list of instructions.
75 | *
76 | * Note: Adding compiled instructions to an existing method with
77 | * offset instructions WITHOUT specifying a parent method will not work.
78 | * @param method The method to compile the instructions against.
79 | * @returns A list of instructions.
80 | */
81 | fun String.toInstructions(method: MutableMethod? = null): List {
82 | return InlineSmaliCompiler.compile(
83 | this,
84 | method?.parameters?.joinToString("") { it } ?: "",
85 | method?.implementation?.registerCount ?: 1,
86 | method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true,
87 | )
88 | }
89 |
90 | /**
91 | * Compile a line of Smali code to an instruction.
92 | * @param templateMethod The method to compile the instructions against.
93 | * @return The instruction.
94 | */
95 | fun String.toInstruction(templateMethod: MutableMethod? = null) = this.toInstructions(templateMethod).first()
96 |
--------------------------------------------------------------------------------
/src/main/resources/app/revanced/patcher/version.properties:
--------------------------------------------------------------------------------
1 | version=${projectVersion}
--------------------------------------------------------------------------------
/src/test/kotlin/app/revanced/patcher/PatcherTest.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher
2 |
3 | import app.revanced.patcher.patch.*
4 | import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
5 | import app.revanced.patcher.util.ProxyClassList
6 | import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
7 | import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
8 | import io.mockk.*
9 | import kotlinx.coroutines.flow.toList
10 | import kotlinx.coroutines.runBlocking
11 | import org.junit.jupiter.api.BeforeEach
12 | import org.junit.jupiter.api.assertAll
13 | import java.util.logging.Logger
14 | import kotlin.test.Test
15 | import kotlin.test.assertEquals
16 | import kotlin.test.assertNotNull
17 | import kotlin.test.assertTrue
18 |
19 | internal object PatcherTest {
20 | private lateinit var patcher: Patcher
21 |
22 | @BeforeEach
23 | fun setUp() {
24 | patcher = mockk {
25 | // Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
26 | setPrivateField(
27 | "config",
28 | mockk {
29 | every { resourceMode } returns ResourcePatchContext.ResourceMode.NONE
30 | },
31 | )
32 | setPrivateField(
33 | "logger",
34 | Logger.getAnonymousLogger(),
35 | )
36 |
37 | every { context.bytecodeContext.classes } returns mockk(relaxed = true)
38 | every { this@mockk() } answers { callOriginal() }
39 | }
40 | }
41 |
42 | @Test
43 | fun `executes patches in correct order`() {
44 | val executed = mutableListOf()
45 |
46 | val patches = setOf(
47 | bytecodePatch { execute { executed += "1" } },
48 | bytecodePatch {
49 | dependsOn(
50 | bytecodePatch {
51 | execute { executed += "2" }
52 | finalize { executed += "-2" }
53 | },
54 | bytecodePatch { execute { executed += "3" } },
55 | )
56 |
57 | execute { executed += "4" }
58 | finalize { executed += "-1" }
59 | },
60 | )
61 |
62 | assert(executed.isEmpty())
63 |
64 | patches()
65 |
66 | assertEquals(
67 | listOf("1", "2", "3", "4", "-1", "-2"),
68 | executed,
69 | "Expected patches to be executed in correct order.",
70 | )
71 | }
72 |
73 | @Test
74 | fun `handles execution of patches correctly when exceptions occur`() {
75 | val executed = mutableListOf()
76 |
77 | infix fun Patch<*>.produces(equals: List) {
78 | val patches = setOf(this)
79 |
80 | patches()
81 |
82 | assertEquals(equals, executed, "Expected patches to be executed in correct order.")
83 |
84 | executed.clear()
85 | }
86 |
87 | // No patches execute successfully,
88 | // because the dependency patch throws an exception inside the execute block.
89 | bytecodePatch {
90 | dependsOn(
91 | bytecodePatch {
92 | execute { throw PatchException("1") }
93 | finalize { executed += "-2" }
94 | },
95 | )
96 |
97 | execute { executed += "2" }
98 | finalize { executed += "-1" }
99 | } produces emptyList()
100 |
101 | // The dependency patch is executed successfully,
102 | // because only the dependant patch throws an exception inside the finalize block.
103 | // Patches that depend on a failed patch should not be executed,
104 | // but patches that are depended on by a failed patch should be executed.
105 | bytecodePatch {
106 | dependsOn(
107 | bytecodePatch {
108 | execute { executed += "1" }
109 | finalize { executed += "-2" }
110 | },
111 | )
112 |
113 | execute { throw PatchException("2") }
114 | finalize { executed += "-1" }
115 | } produces listOf("1", "-2")
116 |
117 | // Because the finalize block of the dependency patch is executed after the finalize block of the dependant patch,
118 | // the dependant patch executes successfully, but the dependency patch raises an exception in the finalize block.
119 | bytecodePatch {
120 | dependsOn(
121 | bytecodePatch {
122 | execute { executed += "1" }
123 | finalize { throw PatchException("-2") }
124 | },
125 | )
126 |
127 | execute { executed += "2" }
128 | finalize { executed += "-1" }
129 | } produces listOf("1", "2", "-1")
130 |
131 | // The dependency patch is executed successfully,
132 | // because the dependant patch raises an exception in the finalize block.
133 | // Patches that depend on a failed patch should not be executed,
134 | // but patches that are depended on by a failed patch should be executed.
135 | bytecodePatch {
136 | dependsOn(
137 | bytecodePatch {
138 | execute { executed += "1" }
139 | finalize { executed += "-2" }
140 | },
141 | )
142 |
143 | execute { executed += "2" }
144 | finalize { throw PatchException("-1") }
145 | } produces listOf("1", "2", "-2")
146 | }
147 |
148 | @Test
149 | fun `throws if unmatched fingerprint match is delegated`() {
150 | val patch = bytecodePatch {
151 | execute {
152 | // Fingerprint can never match.
153 | val fingerprint = fingerprint { }
154 |
155 | // Throws, because the fingerprint can't be matched.
156 | fingerprint.patternMatch
157 | }
158 | }
159 |
160 | assertTrue(
161 | patch().exception != null,
162 | "Expected an exception because the fingerprint can't match.",
163 | )
164 | }
165 |
166 | @Test
167 | fun `matches fingerprint`() {
168 | every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
169 | mutableListOf(
170 | ImmutableClassDef(
171 | "class",
172 | 0,
173 | null,
174 | null,
175 | null,
176 | null,
177 | null,
178 | listOf(
179 | ImmutableMethod(
180 | "class",
181 | "method",
182 | emptyList(),
183 | "V",
184 | 0,
185 | null,
186 | null,
187 | null,
188 | ),
189 | ),
190 | ),
191 | ),
192 | )
193 |
194 | val fingerprint = fingerprint { returns("V") }
195 | val fingerprint2 = fingerprint { returns("V") }
196 | val fingerprint3 = fingerprint { returns("V") }
197 |
198 | val patches = setOf(
199 | bytecodePatch {
200 | execute {
201 | fingerprint.match(classes.first().methods.first())
202 | fingerprint2.match(classes.first())
203 | fingerprint3.originalClassDef
204 | }
205 | },
206 | )
207 |
208 | patches()
209 |
210 | with(patcher.context.bytecodeContext) {
211 | assertAll(
212 | "Expected fingerprints to match.",
213 | { assertNotNull(fingerprint.originalClassDefOrNull) },
214 | { assertNotNull(fingerprint2.originalClassDefOrNull) },
215 | { assertNotNull(fingerprint3.originalClassDefOrNull) },
216 | )
217 | }
218 | }
219 |
220 | private operator fun Set>.invoke(): List {
221 | every { patcher.context.executablePatches } returns toMutableSet()
222 | every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
223 | every { with(patcher.context.bytecodeContext) { mergeExtension(any()) } } just runs
224 |
225 | return runBlocking { patcher().toList() }
226 | }
227 |
228 | private operator fun Patch<*>.invoke() = setOf(this)().first()
229 |
230 | private fun Any.setPrivateField(field: String, value: Any) {
231 | this::class.java.getDeclaredField(field).apply {
232 | this.isAccessible = true
233 | set(this@setPrivateField, value)
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.extensions
2 |
3 | import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
4 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
5 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
6 | import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
7 | import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
8 | import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
9 | import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
10 | import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
11 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
12 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
13 | import app.revanced.patcher.util.smali.ExternalLabel
14 | import com.android.tools.smali.dexlib2.AccessFlags
15 | import com.android.tools.smali.dexlib2.Opcode
16 | import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
17 | import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
18 | import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
19 | import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
20 | import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
21 | import org.junit.jupiter.api.BeforeEach
22 | import kotlin.test.Test
23 | import kotlin.test.assertEquals
24 |
25 | private object InstructionExtensionsTest {
26 | private lateinit var testMethod: MutableMethod
27 | private lateinit var testMethodImplementation: MutableMethodImplementation
28 |
29 | @BeforeEach
30 | fun createTestMethod() =
31 | ImmutableMethod(
32 | "TestClass;",
33 | "testMethod",
34 | null,
35 | "V",
36 | AccessFlags.PUBLIC.value,
37 | null,
38 | null,
39 | MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
40 | repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
41 | },
42 | ).let { testMethod = it.toMutable() }
43 |
44 | @Test
45 | fun addInstructionsToImplementationIndexed() =
46 | applyToImplementation {
47 | addInstructions(5, getTestInstructions(5..6)).also {
48 | assertRegisterIs(5, 5)
49 | assertRegisterIs(6, 6)
50 |
51 | assertRegisterIs(5, 7)
52 | }
53 | }
54 |
55 | @Test
56 | fun addInstructionsToImplementation() =
57 | applyToImplementation {
58 | addInstructions(getTestInstructions(10..11)).also {
59 | assertRegisterIs(10, 10)
60 | assertRegisterIs(11, 11)
61 | }
62 | }
63 |
64 | @Test
65 | fun removeInstructionsFromImplementationIndexed() =
66 | applyToImplementation {
67 | removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
68 | }
69 |
70 | @Test
71 | fun removeInstructionsFromImplementation() =
72 | applyToImplementation {
73 | removeInstructions(0).also { assertRegisterIs(9, 9) }
74 | removeInstructions(1).also { assertRegisterIs(1, 0) }
75 | removeInstructions(2).also { assertRegisterIs(3, 0) }
76 | }
77 |
78 | @Test
79 | fun replaceInstructionsInImplementationIndexed() =
80 | applyToImplementation {
81 | replaceInstructions(5, getTestInstructions(0..1)).also {
82 | assertRegisterIs(0, 5)
83 | assertRegisterIs(1, 6)
84 | assertRegisterIs(7, 7)
85 | }
86 | }
87 |
88 | @Test
89 | fun addInstructionToMethodIndexed() =
90 | applyToMethod {
91 | addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
92 | }
93 |
94 | @Test
95 | fun addInstructionToMethod() =
96 | applyToMethod {
97 | addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
98 | }
99 |
100 | @Test
101 | fun addSmaliInstructionToMethodIndexed() =
102 | applyToMethod {
103 | addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
104 | }
105 |
106 | @Test
107 | fun addSmaliInstructionToMethod() =
108 | applyToMethod {
109 | addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
110 | }
111 |
112 | @Test
113 | fun addInstructionsToMethodIndexed() =
114 | applyToMethod {
115 | addInstructions(5, getTestInstructions(0..1)).also {
116 | assertRegisterIs(0, 5)
117 | assertRegisterIs(1, 6)
118 |
119 | assertRegisterIs(5, 7)
120 | }
121 | }
122 |
123 | @Test
124 | fun addInstructionsToMethod() =
125 | applyToMethod {
126 | addInstructions(getTestInstructions(0..1)).also {
127 | assertRegisterIs(0, 10)
128 | assertRegisterIs(1, 11)
129 |
130 | assertRegisterIs(9, 9)
131 | }
132 | }
133 |
134 | @Test
135 | fun addSmaliInstructionsToMethodIndexed() =
136 | applyToMethod {
137 | addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also {
138 | assertRegisterIs(0, 5)
139 | assertRegisterIs(1, 6)
140 |
141 | assertRegisterIs(5, 7)
142 | }
143 | }
144 |
145 | @Test
146 | fun addSmaliInstructionsToMethod() =
147 | applyToMethod {
148 | addInstructions(getTestSmaliInstructions(0..1)).also {
149 | assertRegisterIs(0, 10)
150 | assertRegisterIs(1, 11)
151 |
152 | assertRegisterIs(9, 9)
153 | }
154 | }
155 |
156 | @Test
157 | fun addSmaliInstructionsWithExternalLabelToMethodIndexed() =
158 | applyToMethod {
159 | val label = ExternalLabel("testLabel", getInstruction(5))
160 |
161 | addInstructionsWithLabels(
162 | 5,
163 | getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
164 | label,
165 | ).also {
166 | assertRegisterIs(0, 5)
167 | assertRegisterIs(1, 6)
168 | assertRegisterIs(5, 8)
169 |
170 | val gotoTarget =
171 | getInstruction(7)
172 | .target.location.instruction as OneRegisterInstruction
173 |
174 | assertEquals(5, gotoTarget.registerA)
175 | }
176 | }
177 |
178 | @Test
179 | fun removeInstructionFromMethodIndexed() =
180 | applyToMethod {
181 | removeInstruction(5).also {
182 | assertRegisterIs(4, 4)
183 | assertRegisterIs(6, 5)
184 | }
185 | }
186 |
187 | @Test
188 | fun removeInstructionsFromMethodIndexed() =
189 | applyToMethod {
190 | removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
191 | }
192 |
193 | @Test
194 | fun removeInstructionsFromMethod() =
195 | applyToMethod {
196 | removeInstructions(0).also { assertRegisterIs(9, 9) }
197 | removeInstructions(1).also { assertRegisterIs(1, 0) }
198 | removeInstructions(2).also { assertRegisterIs(3, 0) }
199 | }
200 |
201 | @Test
202 | fun replaceInstructionInMethodIndexed() =
203 | applyToMethod {
204 | replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
205 | }
206 |
207 | @Test
208 | fun replaceInstructionsInMethodIndexed() =
209 | applyToMethod {
210 | replaceInstructions(5, getTestInstructions(0..1)).also {
211 | assertRegisterIs(0, 5)
212 | assertRegisterIs(1, 6)
213 | assertRegisterIs(7, 7)
214 | }
215 | }
216 |
217 | @Test
218 | fun replaceSmaliInstructionsInMethodIndexed() =
219 | applyToMethod {
220 | replaceInstructions(5, getTestSmaliInstructions(0..1)).also {
221 | assertRegisterIs(0, 5)
222 | assertRegisterIs(1, 6)
223 | assertRegisterIs(7, 7)
224 | }
225 | }
226 |
227 | // region Helper methods
228 |
229 | private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
230 | testMethodImplementation.apply(block)
231 | }
232 |
233 | private fun applyToMethod(block: MutableMethod.() -> Unit) {
234 | testMethod.apply(block)
235 | }
236 |
237 | private fun MutableMethodImplementation.assertRegisterIs(
238 | register: Int,
239 | atIndex: Int,
240 | ) = assertEquals(
241 | register,
242 | getInstruction(atIndex).registerA,
243 | )
244 |
245 | private fun MutableMethod.assertRegisterIs(
246 | register: Int,
247 | atIndex: Int,
248 | ) = implementation!!.assertRegisterIs(register, atIndex)
249 |
250 | private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) }
251 |
252 | private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0"
253 |
254 | private fun getTestSmaliInstructions(range: IntRange) =
255 | range.joinToString("\n") {
256 | getTestSmaliInstruction(it)
257 | }
258 |
259 | // endregion
260 |
261 | private class TestInstruction(register: Int) : BuilderInstruction21s(Opcode.CONST_16, register, 0)
262 | }
263 |
--------------------------------------------------------------------------------
/src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("unused")
2 |
3 | package app.revanced.patcher.patch
4 |
5 | import org.junit.jupiter.api.Test
6 | import java.io.File
7 | import kotlin.reflect.KFunction
8 | import kotlin.reflect.full.companionObject
9 | import kotlin.reflect.full.declaredFunctions
10 | import kotlin.reflect.jvm.isAccessible
11 | import kotlin.reflect.jvm.javaField
12 | import kotlin.test.assertEquals
13 |
14 | // region Test patches.
15 |
16 | // Not loaded, because it's unnamed.
17 | val publicUnnamedPatch = bytecodePatch {
18 | }
19 |
20 | // Loaded, because it's named.
21 | val publicPatch = bytecodePatch("Public") {
22 | }
23 |
24 | // Not loaded, because it's private.
25 | private val privateUnnamedPatch = bytecodePatch {
26 | }
27 |
28 | // Not loaded, because it's private.
29 | private val privatePatch = bytecodePatch("Private") {
30 | }
31 |
32 | // Not loaded, because it's unnamed.
33 | fun publicUnnamedPatchFunction() = publicUnnamedPatch
34 |
35 | // Loaded, because it's named.
36 | fun publicNamedPatchFunction() = bytecodePatch("Public") { }
37 |
38 | // Not loaded, because it's parameterized.
39 | fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) = publicNamedPatchFunction()
40 |
41 | // Not loaded, because it's private.
42 | private fun privateUnnamedPatchFunction() = privateUnnamedPatch
43 |
44 | // Not loaded, because it's private.
45 | private fun privateNamedPatchFunction() = privatePatch
46 |
47 | // endregion
48 |
49 | internal object PatchLoaderTest {
50 | private const val LOAD_PATCHES_FUNCTION_NAME = "loadPatches"
51 | private val TEST_PATCHES_CLASS = ::publicPatch.javaField!!.declaringClass.name
52 | private val TEST_PATCHES_CLASS_LOADER = ::publicPatch.javaClass.classLoader
53 |
54 | @Test
55 | fun `loads patches correctly`() {
56 | // Get instance of private PatchLoader.Companion class.
57 | val patchLoaderCompanionObject = getPrivateFieldByType(
58 | PatchLoader::class.java,
59 | PatchLoader::class.companionObject!!.javaObjectType,
60 | )
61 |
62 | // Get private PatchLoader.Companion.loadPatches function from PatchLoader.Companion.
63 | @Suppress("UNCHECKED_CAST")
64 | val loadPatchesFunction = getPrivateFunctionByName(
65 | patchLoaderCompanionObject,
66 | LOAD_PATCHES_FUNCTION_NAME,
67 | ) as KFunction>>>
68 |
69 | // Call private PatchLoader.Companion.loadPatches function.
70 | val patches = loadPatchesFunction.call(
71 | patchLoaderCompanionObject,
72 | TEST_PATCHES_CLASS_LOADER,
73 | mapOf(File("patchesFile") to setOf(TEST_PATCHES_CLASS)),
74 | ).values.first()
75 |
76 | assertEquals(
77 | 2,
78 | patches.size,
79 | "Expected 2 patches to be loaded, " +
80 | "because there's only two named patches declared as public static fields " +
81 | "or returned by public static and non-parametrized methods.",
82 | )
83 | }
84 |
85 | private fun getPrivateFieldByType(cls: Class<*>, fieldType: Class<*>) =
86 | cls.declaredFields.first { it.type == fieldType }.apply { isAccessible = true }.get(null)
87 |
88 | private fun getPrivateFunctionByName(obj: Any, @Suppress("SameParameterValue") methodName: String) =
89 | obj::class.declaredFunctions.first { it.name == methodName }.apply { isAccessible = true }
90 | }
91 |
--------------------------------------------------------------------------------
/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.patch
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 |
6 | internal object PatchTest {
7 | @Test
8 | fun `can create patch with name`() {
9 | val patch = bytecodePatch(name = "Test") {}
10 |
11 | assertEquals("Test", patch.name)
12 | }
13 |
14 | @Test
15 | fun `can create patch with compatible packages`() {
16 | val patch = bytecodePatch(name = "Test") {
17 | compatibleWith(
18 | "compatible.package"("1.0.0"),
19 | )
20 | }
21 |
22 | assertEquals(1, patch.compatiblePackages!!.size)
23 | assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
24 | }
25 |
26 | @Test
27 | fun `can create patch with dependencies`() {
28 | val patch = bytecodePatch(name = "Test") {
29 | dependsOn(resourcePatch {})
30 | }
31 |
32 | assertEquals(1, patch.dependencies.size)
33 | }
34 |
35 | @Test
36 | fun `can create patch with options`() {
37 | val patch = bytecodePatch(name = "Test") {
38 | val print by stringOption("print")
39 | val custom = option("custom")()
40 |
41 | execute {
42 | println(print)
43 | println(custom.value)
44 | }
45 | }
46 |
47 | assertEquals(2, patch.options.size)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/test/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.patch.options
2 |
3 | import app.revanced.patcher.patch.*
4 | import org.junit.jupiter.api.assertDoesNotThrow
5 | import org.junit.jupiter.api.assertThrows
6 | import kotlin.reflect.typeOf
7 | import kotlin.test.*
8 |
9 | internal object OptionsTest {
10 | private val externalOption = stringOption("external", "default")
11 |
12 | private val optionsTestPatch = bytecodePatch {
13 | externalOption()
14 |
15 | booleanOption("bool", true)
16 |
17 | stringOption("required", "default", required = true)
18 |
19 | stringsOption("list", listOf("1", "2"))
20 |
21 | stringOption("choices", "value", values = mapOf("Valid option value" to "valid"))
22 |
23 | stringOption("validated", "default") { it == "valid" }
24 |
25 | stringOption("resettable", null, required = true)
26 | }
27 |
28 | @Test
29 | fun `should not fail because default value is unvalidated`() = options {
30 | assertDoesNotThrow { get("required") }
31 | }
32 |
33 | @Test
34 | fun `should not allow setting custom value with validation`() = options {
35 | // Getter validation on incorrect value.
36 | assertThrows {
37 | set("validated", get("validated"))
38 | }
39 |
40 | // Setter validation on incorrect value.
41 | assertThrows {
42 | set("validated", "invalid")
43 | }
44 |
45 | // Setter validation on correct value.
46 | assertDoesNotThrow {
47 | set("validated", "valid")
48 | }
49 | }
50 |
51 | @Test
52 | fun `should throw due to incorrect type`() = options {
53 | assertThrows {
54 | set("bool", "not a boolean")
55 | }
56 | }
57 |
58 | @Test
59 | fun `should be nullable`() = options {
60 | assertDoesNotThrow {
61 | set("bool", null)
62 | }
63 | }
64 |
65 | @Test
66 | fun `option should not be found`() = options {
67 | assertThrows {
68 | set("this option does not exist", 1)
69 | }
70 | }
71 |
72 | @Test
73 | fun `should be able to add options manually`() = options {
74 | assertDoesNotThrow {
75 | bytecodePatch {
76 | get("list")()
77 | }.options["list"]
78 | }
79 | }
80 |
81 | @Test
82 | fun `should allow setting value from values`() = options {
83 | @Suppress("UNCHECKED_CAST")
84 | val option = get("choices") as Option
85 |
86 | option.value = option.values!!.values.last()
87 |
88 | assertTrue(option.value == "valid")
89 | }
90 |
91 | @Test
92 | fun `should allow setting custom value`() = options {
93 | assertDoesNotThrow {
94 | set("choices", "unknown")
95 | }
96 | }
97 |
98 | @Test
99 | fun `should allow resetting value`() = options {
100 | assertDoesNotThrow {
101 | set("choices", null)
102 | }
103 |
104 | assert(get("choices").value == null)
105 | }
106 |
107 | @Test
108 | fun `reset should not fail`() = options {
109 | assertDoesNotThrow {
110 | set("resettable", "test")
111 | get("resettable").reset()
112 | }
113 |
114 | assertThrows {
115 | get("resettable").value
116 | }
117 | }
118 |
119 | @Test
120 | fun `option types should be known`() = options {
121 | assertEquals(typeOf>(), get("list").type)
122 | }
123 |
124 | @Test
125 | fun `getting default value should work`() = options {
126 | assertDoesNotThrow {
127 | assertNull(get("resettable").default)
128 | }
129 | }
130 |
131 | @Test
132 | fun `external option should be accessible`() {
133 | assertDoesNotThrow {
134 | externalOption.value = "test"
135 | }
136 | }
137 |
138 | @Test
139 | fun `should allow getting the external option from the patch`() {
140 | assertEquals(optionsTestPatch.options["external"].value, externalOption.value)
141 | }
142 |
143 | private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
144 | }
145 |
--------------------------------------------------------------------------------
/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt:
--------------------------------------------------------------------------------
1 | package app.revanced.patcher.util.smali
2 |
3 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
4 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
5 | import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
6 | import app.revanced.patcher.extensions.newLabel
7 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
8 | import com.android.tools.smali.dexlib2.AccessFlags
9 | import com.android.tools.smali.dexlib2.Opcode
10 | import com.android.tools.smali.dexlib2.builder.BuilderInstruction
11 | import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
12 | import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
13 | import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
14 | import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
15 | import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
16 | import java.util.*
17 | import kotlin.test.Test
18 | import kotlin.test.assertEquals
19 | import kotlin.test.assertTrue
20 |
21 | internal object InlineSmaliCompilerTest {
22 | @Test
23 | fun `outputs valid instruction`() {
24 | val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
25 | val have = "const-string v0, \"Test\"".toInstruction()
26 |
27 | assertInstructionsEqual(want, have)
28 | }
29 |
30 | @Test
31 | fun `supports branching with own branches`() {
32 | val method = createMethod()
33 | val instructionCount = 8
34 | val instructionIndex = instructionCount - 2
35 | val targetIndex = instructionIndex - 1
36 |
37 | method.addInstructions(
38 | arrayOfNulls(instructionCount).also {
39 | Arrays.fill(it, "const/4 v0, 0x0")
40 | }.joinToString("\n"),
41 | )
42 | method.addInstructionsWithLabels(
43 | targetIndex,
44 | """
45 | :test
46 | const/4 v0, 0x1
47 | if-eqz v0, :test
48 | """,
49 | )
50 |
51 | val instruction = method.getInstruction(instructionIndex)
52 |
53 | assertEquals(targetIndex, instruction.target.location.index)
54 | }
55 |
56 | @Test
57 | fun `supports branching to outside branches`() {
58 | val method = createMethod()
59 | val instructionIndex = 3
60 | val labelIndex = 1
61 |
62 | method.addInstructions(
63 | """
64 | const/4 v0, 0x1
65 | const/4 v0, 0x0
66 | """,
67 | )
68 |
69 | assertEquals(labelIndex, method.newLabel(labelIndex).location.index)
70 |
71 | method.addInstructionsWithLabels(
72 | method.implementation!!.instructions.size,
73 | """
74 | const/4 v0, 0x1
75 | if-eqz v0, :test
76 | return-void
77 | """,
78 | ExternalLabel("test", method.getInstruction(1)),
79 | )
80 |
81 | val instruction = method.getInstruction(instructionIndex)
82 | assertTrue(instruction.target.isPlaced, "Label was not placed")
83 | assertEquals(labelIndex, instruction.target.location.index)
84 | }
85 |
86 | private fun createMethod(
87 | name: String = "dummy",
88 | returnType: String = "V",
89 | accessFlags: Int = AccessFlags.STATIC.value,
90 | registerCount: Int = 1,
91 | ) = ImmutableMethod(
92 | "Ldummy;",
93 | name,
94 | emptyList(), // parameters
95 | returnType,
96 | accessFlags,
97 | emptySet(),
98 | emptySet(),
99 | MutableMethodImplementation(registerCount),
100 | ).toMutable()
101 |
102 | private fun assertInstructionsEqual(want: BuilderInstruction, have: BuilderInstruction) {
103 | assertEquals(want.opcode, have.opcode)
104 | assertEquals(want.format, have.format)
105 | assertEquals(want.codeUnits, have.codeUnits)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------