├── .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 | ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-patcher/release.yml) 64 | ![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg) 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 | --------------------------------------------------------------------------------