diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml deleted file mode 100644 index d8c1a0f..0000000 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: 🐞 Bug report -description: Report a very clearly broken issue. -title: 'bug: ' -labels: [bug] -body: - - type: markdown - attributes: - value: | - # ReVanced bug report - - Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-cli/labels/bug). - - - type: dropdown - attributes: - label: Type - options: - - Error while running the CLI - - Error at runtime - - Cosmetic - - Other - validations: - required: true - - type: textarea - attributes: - label: Bug description - description: How did you find the bug? Any additional details that might help? - validations: - required: true - - type: textarea - attributes: - label: Steps to reproduce - description: Add the steps to reproduce this bug including your environment. - placeholder: Step 1. Download some files. Step 2. ... - validations: - required: true - - type: textarea - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. - render: shell - validations: - required: true - - type: textarea - attributes: - label: Screenshots or videos - description: Add screenshots or videos that show the bug here. - placeholder: Drag and drop the screenshots/videos into this box. - validations: - required: false - - type: textarea - attributes: - label: Solution - description: If applicable, add a possible solution. - validations: - required: false - - type: textarea - attributes: - label: Additional context - description: Add additional context here. - validations: - required: false - - type: checkboxes - id: acknowledgements - attributes: - label: Acknowledgements - description: Your issue will be closed if you haven't done these steps. - options: - - label: I have searched the existing issues and this is a new and no duplicate or related to another open issue. - required: true - - label: I have written a short but informative title. - required: true - - label: I filled out all of the requested information in this issue properly. - required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..4a37b91 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,49 @@ +name: 🐞 Bug report +description: Report a bug or an issue. +title: 'bug: ' +labels: ['Bug report'] +body: + - type: markdown + attributes: + value: | + # ReVanced CLI bug report + + Please check for existing bug reports + [here](https://github.com/ReVanced/revanced-cli/labels/Bug%20report) + before creating a new one. + + - type: textarea + attributes: + label: Bug description + description: | + - Describe your bug in detail + - Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...) + - Add images and videos if possible + - List used patches if applicable + validations: + required: true + - type: textarea + attributes: + label: Error logs + description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell. + render: shell + - type: textarea + attributes: + label: Solution + description: If applicable, add a possible solution to the bug. + - type: textarea + attributes: + label: Additional context + description: Add additional context here. + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if you don't follow the checklist below. + options: + - label: This request is not a duplicate of an existing issue. + required: true + - label: I have chosen an appropriate title. + required: true + - label: All requested information has been provided properly. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9f537ff..2482beb 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,5 @@ blank_issues_enabled: false contact_links: - - name: 📃 Documentation - url: https://github.com/revanced/revanced-documentation/ - about: Don't know how or where to start? Check out our documentation! - name: 🗨 Discussions url: https://github.com/revanced/revanced-suggestions/discussions - about: Got something you think should change or be added? Search for or start a new discussion! \ No newline at end of file + about: Have something unspecific to ReVanced CLI in mind? Search for or start a new discussion! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-issue.yml b/.github/ISSUE_TEMPLATE/feature-issue.yml deleted file mode 100644 index 14de652..0000000 --- a/.github/ISSUE_TEMPLATE/feature-issue.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: ⭐ Feature request -description: Create a detailed feature request. -title: 'feat: <title>' -labels: [feature-request] -body: - - type: markdown - attributes: - value: | - # ReVanced feature request - - Do not submit requests for patches here. Please submit them [here](https://github.com/orgs/revanced/discussions/categories/patches) instead. - Important to note that your feature request may have already been made before. Please check for existing feature requests [here](https://github.com/revanced/revanced-cli/labels/feature-request). - - - type: dropdown - attributes: - label: Type - options: - - Functionality - - Cosmetic - - Other - validations: - required: true - - type: textarea - attributes: - label: Issue - description: What is the current problem. Why does it require a feature request? - validations: - required: true - - type: textarea - attributes: - label: Feature - description: Describe your feature in detail. How does it solve the issue? - validations: - required: true - - type: textarea - attributes: - label: Motivation - description: Why should your feature should be considered? - validations: - required: true - - type: textarea - attributes: - label: Additional context - description: Add additional context here. - validations: - required: false - - type: checkboxes - id: acknowledgements - attributes: - label: Acknowledgements - description: Your issue will be closed if you haven't done these steps. - options: - - label: I have searched the existing issues and this is a new and no duplicate or related to another open issue. - required: true - - label: I have written a short but informative title. - required: true - - label: I filled out all of the requested information in this issue properly. - required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..f7ba3c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,45 @@ +name: ⭐ Feature request +description: Create a detailed request for a new feature. +title: 'feat: ' +labels: ['Feature request'] +body: + - type: markdown + attributes: + value: | + # ReVanced CLI feature request + + Please check for existing feature requests + [here](https://github.com/ReVanced/revanced-cli/labels/Feature%20request) + before creating a new one. + + - type: textarea + attributes: + label: Feature description + description: | + - Describe your feature in detail + - Add images, videos, links, examples, references, etc. if possible + - Add the target application name in case you request a new patch + - type: textarea + attributes: + label: Motivation + description: | + A strong motivation is necessary for a feature request to be considered. + + - Why should this feature be implemented? + - What is the explicit use case? + - What are the benefits? + - What makes this feature important? + validations: + required: true + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if you don't follow the checklist below. + options: + - label: This request is not a duplicate of an existing issue. + required: true + - label: I have chosen an appropriate title. + required: true + - label: All requested information has been provided properly. + required: true diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 605e7a7..188fb70 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,4 +1,4 @@ -name: PR to main +name: Open a PR to main on: push: @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main` + MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main` jobs: pull-request: @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Open pull request uses: repo-sync/pull-request@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0861dc6..0214112 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Make sure the release step uses its own credentials: # https://github.com/cycjimmy/semantic-release-action#private-packages diff --git a/CHANGELOG.md b/CHANGELOG.md index f9362b7..635b0bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,71 @@ +# [4.0.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v4.0.0-dev.4...v4.0.0-dev.5) (2023-10-04) + + +### Bug Fixes + +* Only set options for filtered patches ([64d9127](https://github.com/ReVanced/revanced-cli/commit/64d9127291ea9a8abe21a0e82721721495094472)) + + +### Performance Improvements + +* Do not check, if the options file exists twice ([e3c5550](https://github.com/ReVanced/revanced-cli/commit/e3c55507cf52e697b9ce9d59decc1cbe4cfe5b43)) + +# [4.0.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v4.0.0-dev.3...v4.0.0-dev.4) (2023-10-01) + +# [4.0.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v4.0.0-dev.2...v4.0.0-dev.3) (2023-09-27) + +# [4.0.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v4.0.0-dev.1...v4.0.0-dev.2) (2023-09-24) + + +### Features + +* Improve option descriptions ([d5ea5a0](https://github.com/ReVanced/revanced-cli/commit/d5ea5a0ab1cc015730063e5be94ee18bd88cc906)) + +# [4.0.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v3.1.2-dev.1...v4.0.0-dev.1) (2023-09-23) + + +### Bug Fixes + +* Check, if mounting is possible ([3e13fb5](https://github.com/ReVanced/revanced-cli/commit/3e13fb5d56eb2a90c2a4a1ddfc05852b1f70add5)) +* Delete temporal files if it exists ([a022feb](https://github.com/ReVanced/revanced-cli/commit/a022febd0c70807ddc3fa9948207a2a70d5191da)) +* Do not sign if mounting ([578e16b](https://github.com/ReVanced/revanced-cli/commit/578e16b099fddfd2bb56accb225d04dfcd409b0c)) +* Filter logs correctly ([43fc20d](https://github.com/ReVanced/revanced-cli/commit/43fc20d90e0a694b231b17bb7d9ecfa22bb5d9a0)) +* Log logs with levels over warning to error output stream ([075f6ad](https://github.com/ReVanced/revanced-cli/commit/075f6ad56528a667dca1f0bed704cf7e5381026f)) +* Only open files for reading and writing if writeable ([3846f72](https://github.com/ReVanced/revanced-cli/commit/3846f721ca015ab39a7e4b8d3f3d61163a6f1650)) + + +### Features + +* Add function to get the most common compatible version ([77d9173](https://github.com/ReVanced/revanced-cli/commit/77d91735ffbbd6e733f08176f535bfd39ece0c29)) +* Add option to filter patches to be listed by package name ([50c0f98](https://github.com/ReVanced/revanced-cli/commit/50c0f98ce5927e07839437a2e550aa85f5a7e62d)) +* Add option to warn about patches not being found in supplied patch bundles ([e46d855](https://github.com/ReVanced/revanced-cli/commit/e46d85564320f46c6faa54b2d3fa7fca3fa60019)) +* Add ReVanced Library subproject ([#265](https://github.com/ReVanced/revanced-cli/issues/265)) ([157278c](https://github.com/ReVanced/revanced-cli/commit/157278c9ba25f0f786c5fe58e3e23f6890107118)) +* Do not format patch names ([80a8d88](https://github.com/ReVanced/revanced-cli/commit/80a8d88406b2b04d13dca4fb0d7d7d62e1910317)) +* Extend signing API ([592dc1c](https://github.com/ReVanced/revanced-cli/commit/592dc1c64ae4078e73bb71eba11380b301c79dea)) +* Log stacktrace in new line ([c67e3c7](https://github.com/ReVanced/revanced-cli/commit/c67e3c70c7eaa514cde1bebe775a2216bc4a6074)) +* Use ReVanced Library in ReVanced CLI ([7794327](https://github.com/ReVanced/revanced-cli/commit/7794327a11e8a0e0f28176cd45fad797b924c45f)) +* Word log message better ([6942b22](https://github.com/ReVanced/revanced-cli/commit/6942b22a68de5e991987ea89882915917aec93a3)) + + +### BREAKING CHANGES + +* This changes many signatures of existing APIs and adds new functions for signing +* This changes the log handler signature + +# [3.2.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v3.1.2-dev.1...v3.2.0-dev.1) (2023-09-20) + + +### Features + +* Log stacktrace in new line ([c67e3c7](https://github.com/ReVanced/revanced-cli/commit/c67e3c70c7eaa514cde1bebe775a2216bc4a6074)) + +## [3.1.2-dev.1](https://github.com/ReVanced/revanced-cli/compare/v3.1.1...v3.1.2-dev.1) (2023-09-12) + + +### Bug Fixes + +* Log correct options command ([#262](https://github.com/ReVanced/revanced-cli/issues/262)) ([96c196d](https://github.com/ReVanced/revanced-cli/commit/96c196dcb14e37ad91b751af61ee8382547c1ca3)) + ## [3.1.1](https://github.com/ReVanced/revanced-cli/compare/v3.1.0...v3.1.1) (2023-09-09) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8eeb27a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +<p align="center"> + <picture> + <source + width="256px" + media="(prefers-color-scheme: dark)" + srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg" + > + <img + src="assets/revanced-headline/revanced-headline-vertical-light.svg" + > + </picture> + <br> + <a href="https://revanced.app/"> + <img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" /> + </a>    + <a href="https://github.com/revanced"> + <picture> + <source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" /> + <img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" /> + </picture> + </a>    + <a href="http://revanced.app/discord"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" /> + </a>    + <a href="https://reddit.com/r/revancedapp"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" /> + </a>    + <a href="https://t.me/app_revanced"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" /> + </a>    + <a href="https://twitter.com/revancedapp"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032018-6da37214-7474-4641-a1da-7af7db3a31cd.png" /> + </a>    + <a href="https://www.youtube.com/@ReVanced"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" /> + </a>    + <br> + <br> + Continuing the legacy of Vanced +</p> + +# 📙 ReVanced CLI contribution guidelines + +This document describes how to contribute to ReVanced CLI. + +## 📖 Resources to help you get started + +* The [documentation](/docs) explains how to use ReVanced CLI +* [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on +* [Issues](https://github.com/ReVanced/revanced-cli/issues) are where we keep track of bugs and feature requests + +## 🙏 Submitting a feature request + +Features can be requested by opening an issue using the +[Feature request issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Feature+request&projects=&template=feature-request.yml&title=feat%3A+). + +> **Note** +> Requests can be accepted or rejected at the discretion of maintainers of ReVanced CLI. +> Good motivation has to be provided for a request to be accepted. + +## 🐞 Submitting a bug report + +If you encounter a bug while using ReVanced CLI, open an issue using the +[Bug report issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Bug+report&projects=&template=bug-report.yml&title=bug%3A+). + +## 📝 How to contribute + +1. Before contributing, it is recommended to open an issue to discuss your change + with the maintainers of ReVanced CLI. This will help you determine whether your change is acceptable + and whether it is worth your time to implement it +2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev` +3. Commit your changes. +4. Submit a pull request to the `dev` branch of the repository and reference issues + that your pull request closes in the description of your pull request +5. Our team will review your pull request and provide feedback. Once your pull request is approved, + it will be merged into the `dev` branch and will be included in the next release of ReVanced CLI + +❤️ Thank you for considering contributing to ReVanced CLI, +ReVanced \ No newline at end of file diff --git a/README.md b/README.md index 0b6f6f6..f7486d7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,75 @@ +<p align="center"> + <picture> + <source + width="256px" + media="(prefers-color-scheme: dark)" + srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg" + > + <img + src="assets/revanced-headline/revanced-headline-vertical-light.svg" + > + </picture> + <br> + <a href="https://revanced.app/"> + <img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" /> + </a>    + <a href="https://github.com/revanced"> + <picture> + <source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" /> + <img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" /> + </picture> + </a>    + <a href="http://revanced.app/discord"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" /> + </a>    + <a href="https://reddit.com/r/revancedapp"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" /> + </a>    + <a href="https://t.me/app_revanced"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" /> + </a>    + <a href="https://twitter.com/revancedapp"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032018-6da37214-7474-4641-a1da-7af7db3a31cd.png" /> + </a>    + <a href="https://www.youtube.com/@ReVanced"> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" /> + </a>    + <br> + <br> + Continuing the legacy of Vanced +</p> + + # 💻 ReVanced CLI +![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-cli/release.yml) +![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg) + Command line application to use ReVanced. + +## ❓ About + +ReVanced CLI is a command line application to patch apps using ReVanced. +ReVanced CLI also comes with commands to uninstall or install patched apps and list patches from supplied patch bundles. + +## 🚀 Download + +You can download the most recent version of ReVanced CLI from +[here](https://github.com/ReVanced/revanced-cli/releases/latest). Learn how to use ReVanced CLI by following the [documentation](/docs). + +## 📚 Everything else + +### 📙 Contributing + +Thank you for considering contributing to ReVanced CLI. +You can find the contribution guidelines [here](CONTRIBUTING.md). + +### 🛠️ Building + +In order to build ReVanced CLI, you can follow the [documentation](/docs). + +## 📜 Licence + +ReVanced CLI is licensed under the GPLv3 licence. Please see the [licence file](LICENSE) for more information. +[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced CLI as long as you track changes/dates in source files. +Any modifications to ReVanced CLI must also be made available under the GPL along with build & install instructions. diff --git a/build.gradle.kts b/build.gradle.kts index 9c6b335..f6de7ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "1.8.20" + kotlin("jvm") version "1.9.0" alias(libs.plugins.shadow) } @@ -7,13 +7,10 @@ group = "app.revanced" dependencies { implementation(libs.revanced.patcher) - implementation(libs.kotlin.reflect) + implementation(libs.revanced.library) implementation(libs.kotlinx.coroutines.core) implementation(libs.picocli) - implementation(libs.jadb) // Updated fork - implementation(libs.apksig) - implementation(libs.bcpkix.jdk15on) - implementation(libs.jackson.module.kotlin) + testImplementation(libs.kotlin.test) } @@ -46,12 +43,21 @@ tasks { dependsOn(shadowJar) } - // Dummy task to fix the Gradle semantic-release plugin. - // Remove this if you forked it to support building only. - // Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 + /* + Dummy task to hack gradle-semantic-release-plugin to release this project. + + Explanation: + SemVer is a standard for versioning libraries. + For that reason the semantic-release plugin uses the "publish" task to publish libraries. + However, this subproject is not a library, and the "publish" task is not available for this subproject. + Because semantic-release is not designed to handle this case, we need to hack it. + + RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 + */ + register<DefaultTask>("publish") { - group = "publish" - description = "Dummy task" + group = "publishing" + description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced CLI" dependsOn(build) } } diff --git a/gradle.properties b/gradle.properties index 4fa448d..2c335cd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 3.1.1 +version = 4.0.0-dev.5 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0378477..eccdb3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,25 +1,17 @@ [versions] shadow = "8.1.1" -apksig = "8.1.1" -bcpkix-jdk15on = "1.70" -jackson-module-kotlin = "2.14.3" -jadb = "2531a28109" -kotlin-reflect = "1.9.0" kotlin-test = "1.8.20-RC" -kotlinx-coroutines-core = "1.7.1" +kotlinx-coroutines-core = "1.7.3" picocli = "4.7.3" -revanced-patcher = "14.2.1" +revanced-patcher = "16.0.0" +revanced-library = "1.1.1" [libraries] -apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } -bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" } -jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" } -jadb = { module = "com.github.revanced:jadb", version.ref = "jadb" } -kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } +revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" } [plugins] -shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } \ No newline at end of file +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } diff --git a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt index 28419d9..c899aa2 100644 --- a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt @@ -1,13 +1,8 @@ package app.revanced.cli.command import app.revanced.patcher.PatchBundleLoader -import app.revanced.patcher.annotation.Package -import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages -import app.revanced.patcher.extensions.PatchExtensions.description -import app.revanced.patcher.extensions.PatchExtensions.options -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.patch.PatchClass -import app.revanced.patcher.patch.PatchOption +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption import picocli.CommandLine.* import picocli.CommandLine.Help.Visibility.ALWAYS import java.io.File @@ -47,12 +42,17 @@ internal object ListPatchesCommand : Runnable { ) private var withOptions: Boolean = false + @Option( + names = ["-f", "--filter-package-name"], description = ["Filter patches by package name"] + ) + private var packageName: String? = null + override fun run() { - fun Package.buildString() = buildString { - if (withVersions && versions.isNotEmpty()) { + fun Patch.CompatiblePackage.buildString() = buildString { + if (withVersions && versions != null) { appendLine("Package name: $name") appendLine("Compatible versions:") - append(versions.joinToString("\n") { version -> version }.prependIndent("\t")) + append(versions!!.joinToString("\n") { version -> version }.prependIndent("\t")) } else append("Package name: $name") } @@ -66,15 +66,17 @@ internal object ListPatchesCommand : Runnable { } ?: append("Key: $key") } - fun PatchClass.buildString() = buildString { - append("Name: $patchName") + fun Patch<*>.buildString() = buildString { + append("Name: $name") if (withDescriptions) append("\nDescription: $description") - if (withOptions && options != null) { + if (withOptions && options.isNotEmpty()) { appendLine("\nOptions:") append( - options!!.joinToString("\n\n") { option -> option.buildString() }.prependIndent("\t") + options.values.joinToString("\n\n") { option -> + option.buildString() + }.prependIndent("\t") ) } @@ -86,6 +88,12 @@ internal object ListPatchesCommand : Runnable { } } - logger.info(PatchBundleLoader.Jar(*patchBundles).joinToString("\n\n") { it.buildString() }) + fun Patch<*>.anyPackageName(name: String) = compatiblePackages?.any { it.name == name } == true + + val patches = PatchBundleLoader.Jar(*patchBundles) + + val filtered = packageName?.let { patches.filter { patch -> patch.anyPackageName(it) } } ?: patches + + if (filtered.isNotEmpty()) logger.info(filtered.joinToString("\n\n") { it.buildString() }) } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt index 99ac1b1..d8bf6d9 100644 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -1,55 +1,27 @@ package app.revanced.cli.command import app.revanced.cli.command.utility.UtilityCommand -import app.revanced.patcher.patch.PatchClass +import app.revanced.library.logging.Logger import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.IVersionProvider import java.util.* -import java.util.logging.* fun main(args: Array<String>) { - System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n") - Logger.getLogger("").apply { - handlers.forEach { - it.close() - removeHandler(it) - } - - object : Handler() { - override fun publish(record: LogRecord) = formatter.format(record).toByteArray().let { - if (record.level.intValue() > Level.INFO.intValue()) - System.err.write(it) - else - System.out.write(it) - } - - override fun flush() { - System.out.flush() - System.err.flush() - } - - override fun close() = flush() - }.also { - it.level = Level.ALL - it.formatter = SimpleFormatter() - }.let(::addHandler) - } - + Logger.setDefault() CommandLine(MainCommand).execute(*args) } -internal typealias PatchList = List<PatchClass> - private object CLIVersionProvider : IVersionProvider { - override fun getVersion(): Array<String> { - Properties().apply { - load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties")) - }.let { - return arrayOf("ReVanced CLI v${it.getProperty("version")}") - } - } + override fun getVersion() = arrayOf( + MainCommand::class.java.getResourceAsStream( + "/app/revanced/cli/version.properties" + )?.use { stream -> + Properties().apply { load(stream) }.let { + "ReVanced CLI v${it.getProperty("version")}" + } + } ?: "ReVanced CLI") } @Command( diff --git a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt b/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt index 863f8d7..82ca026 100644 --- a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt @@ -1,8 +1,8 @@ package app.revanced.cli.command +import app.revanced.library.Options +import app.revanced.library.Options.setOptions import app.revanced.patcher.PatchBundleLoader -import app.revanced.utils.Options -import app.revanced.utils.Options.setOptions import picocli.CommandLine import picocli.CommandLine.Help.Visibility.ALWAYS import java.io.File @@ -37,10 +37,18 @@ internal object OptionsCommand : Runnable { ) private var update: Boolean = false - override fun run() = if (!filePath.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) { - if (update && filePath.exists()) setOptions(filePath) + override fun run() = try { + PatchBundleLoader.Jar(*patchBundles).let { patches -> + val exists = filePath.exists() + if (!exists || overwrite) { + if (exists && update) patches.setOptions(filePath) - Options.serialize(this, prettyPrint = true).let(filePath::writeText) + Options.serialize(patches, prettyPrint = true).let(filePath::writeText) + } else throw OptionsFileAlreadyExistsException() + } + } catch (ex: OptionsFileAlreadyExistsException) { + logger.severe("Options file already exists, use --overwrite to override it") } - else logger.severe("Options file already exists, use --override to override it") + + class OptionsFileAlreadyExistsException : Exception() } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 9d3796c..3d46c31 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -1,23 +1,18 @@ package app.revanced.cli.command +import app.revanced.library.ApkUtils +import app.revanced.library.Options +import app.revanced.library.Options.setOptions +import app.revanced.library.adb.AdbManager import app.revanced.patcher.PatchBundleLoader +import app.revanced.patcher.PatchSet import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherOptions -import app.revanced.patcher.PatcherResult -import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages -import app.revanced.patcher.extensions.PatchExtensions.include -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.utils.Options -import app.revanced.utils.Options.setOptions -import app.revanced.utils.adb.AdbManager -import app.revanced.utils.align.ZipAligner -import app.revanced.utils.align.zip.ZipFile -import app.revanced.utils.align.zip.structures.ZipEntry -import app.revanced.utils.signing.ApkSigner -import app.revanced.utils.signing.SigningOptions import kotlinx.coroutines.runBlocking import picocli.CommandLine import picocli.CommandLine.Help.Visibility.ALWAYS +import picocli.CommandLine.Model.CommandSpec +import picocli.CommandLine.Spec import java.io.File import java.io.PrintWriter import java.io.StringWriter @@ -30,21 +25,15 @@ import java.util.logging.Logger internal object PatchCommand : Runnable { private val logger = Logger.getLogger(PatchCommand::class.java.name) - @CommandLine.Parameters( - description = ["APK file to be patched"], arity = "1..1" - ) + @Spec + lateinit var spec: CommandSpec // injected by picocli + private lateinit var apk: File - @CommandLine.Option( - names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true - ) - private var patchBundles = emptyList<File>() - - @CommandLine.Option( - names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"] - ) private var integrations = listOf<File>() + private var patchBundles = emptyList<File>() + @CommandLine.Option( names = ["-i", "--include"], description = ["List of patches to include"] ) @@ -69,7 +58,7 @@ internal object PatchCommand : Runnable { @CommandLine.Option( names = ["-f","--force"], - description = ["Force inclusion of patches that are incompatible with the supplied APK file's version"], + description = ["Bypass compatibility checks for the supplied APK's version"], showDefaultValue = ALWAYS ) private var force: Boolean = false @@ -90,22 +79,34 @@ internal object PatchCommand : Runnable { private var mount: Boolean = false @CommandLine.Option( - names = ["--common-name"], - description = ["The common name of the signer of the patched APK file"], + names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"], + ) + private var keystoreFilePath: File? = null + + // key store password + @CommandLine.Option( + names = ["--keystore-password"], + description = ["The password of the keystore to sign the patched APK file with"], + ) + private var keyStorePassword: String? = null // Empty password by default + + @CommandLine.Option( + names = ["--alias"], description = ["The alias of the key from the keystore to sign the patched APK file with"], showDefaultValue = ALWAYS - ) - private var commonName = "ReVanced" + private var alias = "ReVanced Key" @CommandLine.Option( - names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"] + names = ["--keystore-entry-password"], + description = ["The password of the entry from the keystore for the key to sign the patched APK file with"] ) - private var keystorePath: String? = null + private var password = "" // Empty password by default @CommandLine.Option( - names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"] + names = ["--signer"], description = ["The name of the signer to sign the patched APK file with"], + showDefaultValue = ALWAYS ) - private var password = "ReVanced" + private var signer = "ReVanced" @CommandLine.Option( names = ["-r", "--resource-cache"], @@ -114,10 +115,7 @@ internal object PatchCommand : Runnable { ) private var resourceCachePath = File("revanced-resource-cache") - @CommandLine.Option( - names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"] - ) - private var aaptBinaryPath = File("") + private var aaptBinaryPath: File? = null @CommandLine.Option( names = ["-p", "--purge"], @@ -126,221 +124,205 @@ internal object PatchCommand : Runnable { ) private var purge: Boolean = false + @CommandLine.Option( + names = ["-w", "--warn"], + description = ["Warn if a patch can not be found in the supplied patch bundles"], + showDefaultValue = ALWAYS + ) + private var warn: Boolean = false + + @CommandLine.Parameters( + description = ["APK file to be patched"], arity = "1..1" + ) + @Suppress("unused") + private fun setApk(apk: File) { + if (!apk.exists()) throw CommandLine.ParameterException( + spec.commandLine(), + "APK file ${apk.name} does not exist" + ) + this.apk = apk + } + + @CommandLine.Option( + names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"] + ) + @Suppress("unused") + private fun setIntegrations(integrations: Array<File>) { + integrations.firstOrNull { !it.exists() }?.let { + throw CommandLine.ParameterException(spec.commandLine(), "Integrations file ${it.name} does not exist") + } + this.integrations += integrations + } + + @CommandLine.Option( + names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true + ) + @Suppress("unused") + private fun setPatchBundles(patchBundles: Array<File>) { + patchBundles.firstOrNull { !it.exists() }?.let { + throw CommandLine.ParameterException(spec.commandLine(), "Patch bundle ${it.name} does not exist") + } + this.patchBundles = patchBundles.toList() + } + + @CommandLine.Option( + names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"] + ) + @Suppress("unused") + private fun setAaptBinaryPath(aaptBinaryPath: File) { + if (!aaptBinaryPath.exists()) throw CommandLine.ParameterException( + spec.commandLine(), + "AAPT binary ${aaptBinaryPath.name} does not exist" + ) + this.aaptBinaryPath = aaptBinaryPath + } + override fun run() { - // region Prepare - - if (!apk.exists()) { - logger.severe("APK file ${apk.name} does not exist") - return - } - - integrations.filter { !it.exists() }.let { - if (it.isEmpty()) return@let - - it.forEach { integration -> - logger.severe("Integration file ${integration.name} does not exist") - } - return - } - - val adbManager = deviceSerial?.let { serial -> - if (mount) AdbManager.RootAdbManager(serial) - else AdbManager.UserAdbManager(serial) - } - - // endregion + val adbManager = deviceSerial?.let { serial -> AdbManager.getAdbManager(serial, mount) } // region Load patches logger.info("Loading patches") val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray()) - val integrations = integrations - logger.info("Setting patch options") - - optionsFile.let { - if (it.exists()) patches.setOptions(it) - else Options.serialize(patches, prettyPrint = true).let(it::writeText) + // Warn if a patch can not be found in the supplied patch bundles. + if (warn) patches.map { it.name }.toHashSet().let { availableNames -> + arrayOf(*includedPatches, *excludedPatches).filter { name -> + !availableNames.contains(name) + } + }.let { unknownPatches -> + if (unknownPatches.isEmpty()) return@let + logger.warning("Unknown input of patches:\n${unknownPatches.joinToString("\n")}") } // endregion - // region Patch - - val patcher = Patcher( + Patcher( PatcherOptions( apk, resourceCachePath, - aaptBinaryPath.path, + aaptBinaryPath?.path, resourceCachePath.absolutePath, ) - ) + ).use { patcher -> + val filteredPatches = patcher.filterPatchSelection(patches).also { patches -> + logger.info("Setting patch options") - val result = patcher.apply { - acceptIntegrations(integrations) - acceptPatches(filterPatchSelection(patches)) - - // Execute patches. - runBlocking { - apply(false).collect { patchResult -> - patchResult.exception?.let { - StringWriter().use { writer -> - it.printStackTrace(PrintWriter(writer)) - logger.severe("${patchResult.patchName} failed: $writer") - } - } ?: logger.info("${patchResult.patchName} succeeded") - } + if (optionsFile.exists()) patches.setOptions(optionsFile) + else Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText) } - }.get() - patcher.close() + // region Patch - // endregion + val patcherResult = patcher.apply { + acceptIntegrations(integrations) + acceptPatches(filteredPatches.toList()) - // region Finish + // Execute patches. + runBlocking { + apply(false).collect { patchResult -> + patchResult.exception?.let { + StringWriter().use { writer -> + it.printStackTrace(PrintWriter(writer)) + logger.severe("${patchResult.patch.name} failed:\n$writer") + } + } ?: logger.info("${patchResult.patch.name} succeeded") + } + } + }.get() - val alignAndSignedFile = sign( - apk.newAlignedFile( - result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk") + // endregion + + // region Save + + val tempFile = resourceCachePath.resolve(apk.name).apply { + ApkUtils.copyAligned(apk, this, patcherResult) + } + + val keystoreFilePath = keystoreFilePath ?: outputFilePath.absoluteFile.parentFile + .resolve("${outputFilePath.nameWithoutExtension}.keystore") + + if (!mount) ApkUtils.sign( + tempFile, + outputFilePath, + ApkUtils.SigningOptions( + keystoreFilePath, + keyStorePassword, + alias, + password, + signer + ) ) - ) - logger.info("Copying to ${outputFilePath.name}") - alignAndSignedFile.copyTo(outputFilePath, overwrite = true) + // endregion - adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName)) + // region Install + + adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName)) + + // endregion + } if (purge) { logger.info("Purging temporary files") purge(resourceCachePath) } - - // endregion } /** * Filter the patches to be added to the patcher. The filter is based on the following: - * - [includedPatches] (explicitly included) - * - [excludedPatches] (explicitly excluded) - * - [exclusive] (only include patches that are explicitly included) - * - [force] (ignore patches incompatibility to versions) - * - Package name and version of the input APK file (if [force] is false) * * @param patches The patches to filter. * @return The filtered patches. */ - private fun Patcher.filterPatchSelection(patches: PatchList) = buildList { - // TODO: Remove this eventually because - // patches named "patch-name" and "patch name" will conflict. - fun String.format() = lowercase().replace(" ", "-") - - val formattedExcludedPatches = excludedPatches.map { it.format() } - val formattedIncludedPatches = includedPatches.map { it.format() } - + private fun Patcher.filterPatchSelection(patches: PatchSet): PatchSet = buildSet { val packageName = context.packageMetadata.packageName val packageVersion = context.packageMetadata.packageVersion patches.forEach patch@{ patch -> - val formattedPatchName = patch.patchName.format() + val patchName = patch.name!! - val explicitlyExcluded = formattedExcludedPatches.contains(formattedPatchName) - if (explicitlyExcluded) return@patch logger.info("Excluding ${patch.patchName}") + val explicitlyExcluded = excludedPatches.contains(patchName) + if (explicitlyExcluded) return@patch logger.info("Excluding $patchName") // Make sure the patch is compatible with the supplied APK files package name and version. patch.compatiblePackages?.let { packages -> packages.singleOrNull { it.name == packageName }?.let { `package` -> - val matchesVersion = force || `package`.versions.let { - it.isEmpty() || it.any { version -> version == packageVersion } - } + val matchesVersion = force || `package`.versions?.let { + it.any { version -> version == packageVersion } + } ?: true if (!matchesVersion) return@patch logger.warning( - "${patch.patchName} is incompatible with version $packageVersion. " + "$patchName is incompatible with version $packageVersion. " + "This patch is only compatible with version " + packages.joinToString(";") { pkg -> - "${pkg.name}: ${pkg.versions.joinToString(", ")}" + "${pkg.name}: ${pkg.versions!!.joinToString(", ")}" } ) - } ?: return@patch logger.fine("${patch.patchName} is incompatible with $packageName. " + } ?: return@patch logger.fine( + "$patchName is incompatible with $packageName. " + "This patch is only compatible with " + packages.joinToString(", ") { `package` -> `package`.name }) return@let - } ?: logger.fine("$formattedPatchName: No constraint on packages.") + } ?: logger.fine("$patchName has no constraint on packages.") - // If the patch is implicitly included, it will be only included if [exclusive] is false. - val implicitlyIncluded = !exclusive && patch.include - // If the patch is explicitly included, it will be included even if [exclusive] is false. - val explicitlyIncluded = formattedIncludedPatches.contains(formattedPatchName) + // If the patch is implicitly used, it will be only included if [exclusive] is false. + val implicitlyIncluded = !exclusive && patch.use + // If the patch is explicitly used, it will be included even if [exclusive] is false. + val explicitlyIncluded = includedPatches.contains(patchName) val included = implicitlyIncluded || explicitlyIncluded - if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1. + if (!included) return@patch logger.info("$patchName excluded") // Case 1. - logger.fine("Adding $formattedPatchName") + logger.fine("Adding $patchName") add(patch) } } - /** - * Create a new aligned APK file. - * - * @param result The result of the patching process. - * @param outputFile The file to save the aligned APK to. - */ - private fun File.newAlignedFile( - result: PatcherResult, outputFile: File - ): File { - logger.info("Aligning $name") - - if (outputFile.exists()) outputFile.delete() - - ZipFile(outputFile).use { file -> - result.dexFiles.forEach { - file.addEntryCompressData( - ZipEntry.createWithName(it.name), it.stream.readBytes() - ) - } - - result.resourceFile?.let { - file.copyEntriesFromFileAligned( - ZipFile(it), ZipAligner::getEntryAlignment - ) - } - - // TODO: Do not compress result.doNotCompress - - file.copyEntriesFromFileAligned( - ZipFile(this), ZipAligner::getEntryAlignment - ) - } - - return outputFile - } - - /** - * Sign the APK file. - * - * @param inputFile The APK file to sign. - * @return The signed APK file. If [mount] is true, the input file will be returned. - */ - private fun sign(inputFile: File) = if (mount) inputFile - else { - logger.info("Signing ${inputFile.name}") - - val keyStoreFilePath = keystorePath - ?: outputFilePath.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath - - val options = SigningOptions( - commonName, password, keyStoreFilePath - ) - - ApkSigner(options).signApk( - inputFile, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk") - ) - } - private fun purge(resourceCachePath: File) { val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory" else "Failed to purge resource cache directory" diff --git a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt index 593680d..59a83dc 100644 --- a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt @@ -1,6 +1,6 @@ package app.revanced.cli.command.utility -import app.revanced.utils.adb.AdbManager +import app.revanced.library.adb.AdbManager import picocli.CommandLine.* import java.io.File import java.util.logging.Logger @@ -28,15 +28,11 @@ internal object InstallCommand : Runnable { ) private var packageName: String? = null - override fun run() = try { - deviceSerials.forEach { deviceSerial -> - if (packageName != null) { - AdbManager.RootAdbManager(deviceSerial) - } else { - AdbManager.UserAdbManager(deviceSerial) - }.install(AdbManager.Apk(apk, packageName)) + override fun run() = deviceSerials.forEach { deviceSerial -> + try { + AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName)) + } catch (e: AdbManager.DeviceNotFoundException) { + logger.severe(e.toString()) } - } catch (e: AdbManager.DeviceNotFoundException) { - logger.severe(e.toString()) } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt index e17bec7..d89b050 100644 --- a/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt @@ -1,6 +1,6 @@ package app.revanced.cli.command.utility -import app.revanced.utils.adb.AdbManager +import app.revanced.library.adb.AdbManager import picocli.CommandLine.* import picocli.CommandLine.Help.Visibility.ALWAYS import java.util.logging.Logger @@ -26,15 +26,11 @@ internal object UninstallCommand : Runnable { ) private var unmount: Boolean = false - override fun run() = try { - deviceSerials.forEach { deviceSerial -> - if (unmount) { - AdbManager.RootAdbManager(deviceSerial) - } else { - AdbManager.UserAdbManager(deviceSerial) - }.uninstall(packageName) + override fun run() = deviceSerials.forEach { deviceSerial -> + try { + AdbManager.getAdbManager(deviceSerial, unmount).uninstall(packageName) + } catch (e: AdbManager.DeviceNotFoundException) { + logger.severe(e.toString()) } - } catch (e: AdbManager.DeviceNotFoundException) { - logger.severe(e.toString()) } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/Options.kt b/src/main/kotlin/app/revanced/utils/Options.kt deleted file mode 100644 index 408e203..0000000 --- a/src/main/kotlin/app/revanced/utils/Options.kt +++ /dev/null @@ -1,110 +0,0 @@ -package app.revanced.utils - -import app.revanced.cli.command.PatchList -import app.revanced.patcher.extensions.PatchExtensions.options -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.patch.NoSuchOptionException -import app.revanced.utils.Options.PatchOption.Option -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import java.io.File -import java.util.logging.Logger - - -internal object Options { - private val logger = Logger.getLogger(Options::class.java.name) - - private var mapper = jacksonObjectMapper() - - /** - * Serializes the options for the patches in the list. - * - * @param patches The list of patches to serialize. - * @param prettyPrint Whether to pretty print the JSON. - * @return The JSON string containing the options. - * @see PatchList - */ - fun serialize(patches: PatchList, prettyPrint: Boolean = false): String = patches - .filter { it.options?.any() == true } - .map { patch -> - PatchOption( - patch.patchName, - patch.options!!.map { option -> Option(option.key, option.value) } - ) - } - // See https://github.com/revanced/revanced-patches/pull/2434/commits/60e550550b7641705e81aa72acfc4faaebb225e7. - .distinctBy { it.patchName } - .let { - if (prettyPrint) - mapper.writerWithDefaultPrettyPrinter().writeValueAsString(it) - else - mapper.writeValueAsString(it) - } - - /** - * Deserializes the options for the patches in the list. - * - * @param json The JSON string containing the options. - * @return The list of [PatchOption]s. - * @see PatchOption - * @see PatchList - */ - @Suppress("MemberVisibilityCanBePrivate") - fun deserialize(json: String): Array<PatchOption> = mapper.readValue(json, Array<PatchOption>::class.java) - - /** - * Sets the options for the patches in the list. - * - * @param json The JSON string containing the options. - */ - fun PatchList.setOptions(json: String) { - filter { it.options?.any() == true }.let { patches -> - if (patches.isEmpty()) return - - val patchOptions = deserialize(json) - - patches.forEach patch@{ patch -> - patchOptions.find { option -> option.patchName == patch.patchName }?.let { - it.options.forEach { option -> - try { - patch.options?.set(option.key, option.value) - ?: run{ - logger.warning("${patch.patchName} has no options") - return@patch - } - } catch (e: NoSuchOptionException) { - logger.info(e.toString()) - } - } - } - } - } - } - - /** - * Sets the options for the patches in the list. - * - * @param file The file containing the JSON string containing the options. - * @see setOptions - */ - fun PatchList.setOptions(file: File) = setOptions(file.readText()) - - /** - * Data class for a patch and its [Option]s. - * - * @property patchName The name of the patch. - * @property options The [Option]s for the patch. - */ - internal data class PatchOption( - val patchName: String, - val options: List<Option> - ) { - - /** - * Data class for patch option. - * - * @property key The name of the option. - * @property value The value of the option. - */ - internal data class Option(val key: String, val value: Any?) - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt deleted file mode 100644 index 1bd0043..0000000 --- a/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt +++ /dev/null @@ -1,140 +0,0 @@ -package app.revanced.utils.adb - -import app.revanced.utils.adb.AdbManager.Apk -import app.revanced.utils.adb.Constants.CREATE_DIR -import app.revanced.utils.adb.Constants.DELETE -import app.revanced.utils.adb.Constants.INSTALLATION_PATH -import app.revanced.utils.adb.Constants.INSTALL_MOUNT -import app.revanced.utils.adb.Constants.INSTALL_PATCHED_APK -import app.revanced.utils.adb.Constants.MOUNT_PATH -import app.revanced.utils.adb.Constants.MOUNT_SCRIPT -import app.revanced.utils.adb.Constants.PATCHED_APK_PATH -import app.revanced.utils.adb.Constants.PLACEHOLDER -import app.revanced.utils.adb.Constants.RESTART -import app.revanced.utils.adb.Constants.TMP_PATH -import app.revanced.utils.adb.Constants.UMOUNT -import se.vidstige.jadb.JadbConnection -import se.vidstige.jadb.managers.Package -import se.vidstige.jadb.managers.PackageManager -import java.io.Closeable -import java.io.File -import java.util.logging.Logger - -/** - * Adb manager. Used to install and uninstall [Apk] files. - * - * @param deviceSerial The serial of the device. - */ -internal sealed class AdbManager(deviceSerial: String? = null) : Closeable { - protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name) - - protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial } - ?: throw DeviceNotFoundException(deviceSerial) - - init { - logger.fine("Established connection to $deviceSerial") - } - - /** - * Installs the [Apk] file. - * - * @param apk The [Apk] file. - */ - open fun install(apk: Apk) { - logger.info("Finished installing ${apk.file.name}") - } - - /** - * Uninstalls the package. - * - * @param packageName The package name. - */ - open fun uninstall(packageName: String) { - logger.info("Finished uninstalling $packageName") - } - - /** - * Closes the [AdbManager] instance. - */ - override fun close() { - logger.fine("Closed") - } - - class RootAdbManager(deviceSerial: String) : AdbManager(deviceSerial) { - init { - if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed") - } - - override fun install(apk: Apk) { - logger.info("Installing by mounting") - - val applyReplacement = getPlaceholderReplacement( - apk.packageName ?: throw IllegalArgumentException("Package name is required") - ) - - device.push(apk.file, TMP_PATH) - - device.run("$CREATE_DIR $INSTALLATION_PATH") - device.run(INSTALL_PATCHED_APK.applyReplacement()) - - device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement()) - - device.run(INSTALL_MOUNT.applyReplacement()) - device.run(UMOUNT.applyReplacement()) // Sanity check. - device.run(MOUNT_PATH.applyReplacement()) - device.run(RESTART.applyReplacement()) - device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement()) - - super.install(apk) - } - - override fun uninstall(packageName: String) { - logger.info("Uninstalling $packageName by unmounting") - - val applyReplacement = getPlaceholderReplacement(packageName) - - device.run(UMOUNT.applyReplacement(packageName)) - device.run(DELETE.applyReplacement(PATCHED_APK_PATH).applyReplacement()) - device.run(DELETE.applyReplacement(MOUNT_PATH).applyReplacement()) - device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement()) - - super.uninstall(packageName) - } - - companion object Utils { - private fun getPlaceholderReplacement(with: String): String.() -> String = { replace(PLACEHOLDER, with) } - private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with) - } - } - - class UserAdbManager(deviceSerial: String) : AdbManager(deviceSerial) { - private val packageManager = PackageManager(device) - - override fun install(apk: Apk) { - PackageManager(device).install(apk.file) - - super.install(apk) - } - - override fun uninstall(packageName: String) { - logger.info("Uninstalling $packageName") - - packageManager.uninstall(Package(packageName)) - - super.uninstall(packageName) - } - } - - /** - * Apk file for [AdbManager]. - * - * @param file The [Apk] file. - * @param packageName The package name of the [Apk] file. - */ - internal class Apk(val file: File, val packageName: String? = null) - - internal class DeviceNotFoundException(deviceSerial: String?) : - Exception(deviceSerial?.let { - "The device with the ADB device serial \"$deviceSerial\" can not be found" - } ?: "No ADB device found") -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Commands.kt b/src/main/kotlin/app/revanced/utils/adb/Commands.kt deleted file mode 100644 index 7ec7eb8..0000000 --- a/src/main/kotlin/app/revanced/utils/adb/Commands.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.revanced.utils.adb - -import se.vidstige.jadb.JadbDevice -import se.vidstige.jadb.RemoteFile -import se.vidstige.jadb.ShellProcessBuilder -import java.io.File - - -internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { - if (su) return shellProcessBuilder("su -c \'$command\'") - - val args = command.split(" ") as ArrayList<String> - val cmd = args.removeFirst() - - return shellProcessBuilder(cmd, *args.toTypedArray()) -} - -internal fun JadbDevice.run(command: String, su: Boolean = true): Int { - return this.buildCommand(command, su).start().waitFor() -} - -internal fun JadbDevice.hasSu() = - this.startCommand("su -h", false).waitFor() == 0 - -internal fun JadbDevice.push(file: File, targetFilePath: String) = - push(file, RemoteFile(targetFilePath)) - -internal fun JadbDevice.createFile(targetFile: String, content: String) = - push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) - - -private fun JadbDevice.startCommand(command: String, su: Boolean) = - shellProcessBuilder(if (su) "su -c '$command'" else command).start() \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Constants.kt b/src/main/kotlin/app/revanced/utils/adb/Constants.kt deleted file mode 100644 index ea5095e..0000000 --- a/src/main/kotlin/app/revanced/utils/adb/Constants.kt +++ /dev/null @@ -1,40 +0,0 @@ -package app.revanced.utils.adb - -internal object Constants { - internal const val PLACEHOLDER = "PLACEHOLDER" - - internal const val TMP_PATH = "/data/local/tmp/revanced.tmp" - internal const val INSTALLATION_PATH = "/data/adb/revanced/" - internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk" - internal const val MOUNT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" - - internal const val DELETE = "rm -rf $PLACEHOLDER" - internal const val CREATE_DIR = "mkdir -p" - internal const val RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | " + - "xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)" - - internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " + - "mv $TMP_PATH ${'$'}base_path && " + - "chmod 644 ${'$'}base_path && " + - "chown system:system ${'$'}base_path && " + - "chcon u:object_r:apk_data_file:s0 ${'$'}base_path" - - internal const val UMOUNT = - "grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done" - - internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH" - - internal val MOUNT_SCRIPT = - """ - #!/system/bin/sh - MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin - MIRROR="${'$'}MAGISKTMP/.magisk/mirror" - while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done - - base_path="$PATCHED_APK_PATH" - stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) - - chcon u:object_r:apk_data_file:s0 ${'$'}base_path - mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path - """.trimIndent() -} diff --git a/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt b/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt deleted file mode 100644 index 568e20b..0000000 --- a/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.utils.align - -import app.revanced.utils.align.zip.structures.ZipEntry - -internal object ZipAligner { - private const val DEFAULT_ALIGNMENT = 4 - private const val LIBRARY_ALIGNMENT = 4096 - - fun getEntryAlignment(entry: ZipEntry): Int? = - if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT -} diff --git a/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt b/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt deleted file mode 100644 index 330c689..0000000 --- a/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.revanced.utils.align.zip - -import java.io.DataInput -import java.io.DataOutput -import java.nio.ByteBuffer - -fun UInt.toLittleEndian() = - (((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt() - -fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort() - -fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8) - or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt() - -fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort() - -fun ByteBuffer.getUShort() = this.getShort().toUShort() -fun ByteBuffer.getUInt() = this.getInt().toUInt() - -fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort()) -fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt()) - -fun DataInput.readUShort() = this.readShort().toUShort() -fun DataInput.readUInt() = this.readInt().toUInt() - -fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt()) -fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt()) - -fun DataInput.readUShortLE() = this.readUShort().toBigEndian() -fun DataInput.readUIntLE() = this.readUInt().toBigEndian() - -fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian()) -fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian()) diff --git a/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt b/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt deleted file mode 100644 index f961488..0000000 --- a/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt +++ /dev/null @@ -1,181 +0,0 @@ -package app.revanced.utils.align.zip - -import app.revanced.utils.align.zip.structures.ZipEndRecord -import app.revanced.utils.align.zip.structures.ZipEntry -import java.io.Closeable -import java.io.File -import java.io.RandomAccessFile -import java.nio.ByteBuffer -import java.nio.channels.FileChannel -import java.util.zip.CRC32 -import java.util.zip.Deflater - -class ZipFile(file: File) : Closeable { - private var entries: MutableList<ZipEntry> = mutableListOf() - - private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") - private var centralDirectoryNeedsRewrite = false - - private val compressionLevel = 5 - - init { - // If file isn't empty try to load entries. - if (file.length() > 0) { - val endRecord = findEndRecord() - - if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries) - throw IllegalArgumentException("Multi-file archives are not supported") - - entries = readEntries(endRecord).toMutableList() - } - - // Seek back to start for writing. - filePointer.seek(0) - } - - private fun findEndRecord(): ZipEndRecord { - // Look from end to start since end record is at the end. - for (i in filePointer.length() - 1 downTo 0) { - filePointer.seek(i) - // Possible beginning of signature. - if (filePointer.readByte() == 0x50.toByte()) { - // Seek back to get the full int. - filePointer.seek(i) - val possibleSignature = filePointer.readUIntLE() - if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { - filePointer.seek(i) - return ZipEndRecord.fromECD(filePointer) - } - } - } - - throw Exception("Couldn't find end record") - } - - private fun readEntries(endRecord: ZipEndRecord): List<ZipEntry> { - filePointer.seek(endRecord.centralDirectoryStartOffset.toLong()) - - val numberOfEntries = endRecord.diskEntries.toInt() - - return buildList(numberOfEntries) { - for (i in 1..numberOfEntries) { - add( - ZipEntry.fromCDE(filePointer).also - { - //for some reason the local extra field can be different from the central one - it.readLocalExtra( - filePointer.channel.map( - FileChannel.MapMode.READ_ONLY, - it.localHeaderOffset.toLong() + 28, - 2 - ) - ) - }) - } - } - } - - private fun writeCD() { - val centralDirectoryStartOffset = filePointer.channel.position().toUInt() - - entries.forEach { - filePointer.channel.write(it.toCDE()) - } - - val entriesCount = entries.size.toUShort() - - val endRecord = ZipEndRecord( - 0u, - 0u, - entriesCount, - entriesCount, - filePointer.channel.position().toUInt() - centralDirectoryStartOffset, - centralDirectoryStartOffset, - "" - ) - - filePointer.channel.write(endRecord.toECD()) - } - - private fun addEntry(entry: ZipEntry, data: ByteBuffer) { - centralDirectoryNeedsRewrite = true - - entry.localHeaderOffset = filePointer.channel.position().toUInt() - - filePointer.channel.write(entry.toLFH()) - filePointer.channel.write(data) - - entries.add(entry) - } - - fun addEntryCompressData(entry: ZipEntry, data: ByteArray) { - val compressor = Deflater(compressionLevel, true) - compressor.setInput(data) - compressor.finish() - - val uncompressedSize = data.size - val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger. - - val compressedDataLength = compressor.deflate(compressedData) - val compressedBuffer = - ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray()) - - compressor.end() - - val crc = CRC32() - crc.update(data) - - entry.compression = 8u // Deflate compression. - entry.uncompressedSize = uncompressedSize.toUInt() - entry.compressedSize = compressedDataLength.toUInt() - entry.crc32 = crc.value.toUInt() - - addEntry(entry, compressedBuffer) - } - - private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) { - alignment?.let { - // Calculate where data would end up. - val dataOffset = filePointer.filePointer + entry.LFHSize - - val mod = dataOffset % alignment - - // Wrong alignment. - if (mod != 0L) { - // Add padding at end of extra field. - entry.localExtraField = - entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt()) - } - } - - addEntry(entry, data) - } - - private fun getDataForEntry(entry: ZipEntry): ByteBuffer { - return filePointer.channel.map( - FileChannel.MapMode.READ_ONLY, - entry.dataOffset.toLong(), - entry.compressedSize.toLong() - ) - } - - /** - * Copies all entries from [file] to this file but skip already existing entries. - * - * @param file The file to copy entries from. - * @param entryAlignment A function that returns the alignment for a given entry. - */ - fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) { - for (entry in file.entries) { - if (entries.any { it.fileName == entry.fileName }) continue // Skip duplicates - - val data = file.getDataForEntry(entry) - addEntryCopyData(entry, data, entryAlignment(entry)) - } - } - - override fun close() { - if (centralDirectoryNeedsRewrite) writeCD() - filePointer.close() - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt deleted file mode 100644 index 387679e..0000000 --- a/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt +++ /dev/null @@ -1,77 +0,0 @@ -package app.revanced.utils.align.zip.structures - -import app.revanced.utils.align.zip.putUInt -import app.revanced.utils.align.zip.putUShort -import app.revanced.utils.align.zip.readUIntLE -import app.revanced.utils.align.zip.readUShortLE -import java.io.DataInput -import java.nio.ByteBuffer -import java.nio.ByteOrder - -data class ZipEndRecord( - val diskNumber: UShort, - val startingDiskNumber: UShort, - val diskEntries: UShort, - val totalEntries: UShort, - val centralDirectorySize: UInt, - val centralDirectoryStartOffset: UInt, - val fileComment: String, -) { - - companion object { - const val ECD_HEADER_SIZE = 22 - const val ECD_SIGNATURE = 0x06054b50u - - fun fromECD(input: DataInput): ZipEndRecord { - val signature = input.readUIntLE() - - if (signature != ECD_SIGNATURE) - throw IllegalArgumentException("Input doesn't start with end record signature") - - val diskNumber = input.readUShortLE() - val startingDiskNumber = input.readUShortLE() - val diskEntries = input.readUShortLE() - val totalEntries = input.readUShortLE() - val centralDirectorySize = input.readUIntLE() - val centralDirectoryStartOffset = input.readUIntLE() - val fileCommentLength = input.readUShortLE() - var fileComment = "" - - if (fileCommentLength > 0u) { - val fileCommentBytes = ByteArray(fileCommentLength.toInt()) - input.readFully(fileCommentBytes) - fileComment = fileCommentBytes.toString(Charsets.UTF_8) - } - - return ZipEndRecord( - diskNumber, - startingDiskNumber, - diskEntries, - totalEntries, - centralDirectorySize, - centralDirectoryStartOffset, - fileComment - ) - } - } - - fun toECD(): ByteBuffer { - val commentBytes = fileComment.toByteArray(Charsets.UTF_8) - - val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size).also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(ECD_SIGNATURE) - buffer.putUShort(diskNumber) - buffer.putUShort(startingDiskNumber) - buffer.putUShort(diskEntries) - buffer.putUShort(totalEntries) - buffer.putUInt(centralDirectorySize) - buffer.putUInt(centralDirectoryStartOffset) - buffer.putUShort(commentBytes.size.toUShort()) - - buffer.put(commentBytes) - - buffer.flip() - return buffer - } -} diff --git a/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt deleted file mode 100644 index 316a836..0000000 --- a/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt +++ /dev/null @@ -1,189 +0,0 @@ -package app.revanced.utils.align.zip.structures - -import app.revanced.utils.align.zip.* -import java.io.DataInput -import java.nio.ByteBuffer -import java.nio.ByteOrder - -data class ZipEntry( - val version: UShort, - val versionNeeded: UShort, - val flags: UShort, - var compression: UShort, - val modificationTime: UShort, - val modificationDate: UShort, - var crc32: UInt, - var compressedSize: UInt, - var uncompressedSize: UInt, - val diskNumber: UShort, - val internalAttributes: UShort, - val externalAttributes: UInt, - var localHeaderOffset: UInt, - val fileName: String, - val extraField: ByteArray, - val fileComment: String, - var localExtraField: ByteArray = ByteArray(0), //separate for alignment -) { - val LFHSize: Int - get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size - - val dataOffset: UInt - get() = localHeaderOffset + LFHSize.toUInt() - - companion object { - const val CDE_HEADER_SIZE = 46 - const val CDE_SIGNATURE = 0x02014b50u - - const val LFH_HEADER_SIZE = 30 - const val LFH_SIGNATURE = 0x04034b50u - - fun createWithName(fileName: String): ZipEntry { - return ZipEntry( - 0x1403u, //made by unix, version 20 - 0u, - 0u, - 0u, - 0x0821u, //seems to be static time google uses, no idea - 0x0221u, //same as above - 0u, - 0u, - 0u, - 0u, - 0u, - 0u, - 0u, - fileName, - ByteArray(0), - "" - ) - } - - fun fromCDE(input: DataInput): ZipEntry { - val signature = input.readUIntLE() - - if (signature != CDE_SIGNATURE) - throw IllegalArgumentException("Input doesn't start with central directory entry signature") - - val version = input.readUShortLE() - val versionNeeded = input.readUShortLE() - var flags = input.readUShortLE() - val compression = input.readUShortLE() - val modificationTime = input.readUShortLE() - val modificationDate = input.readUShortLE() - val crc32 = input.readUIntLE() - val compressedSize = input.readUIntLE() - val uncompressedSize = input.readUIntLE() - val fileNameLength = input.readUShortLE() - var fileName = "" - val extraFieldLength = input.readUShortLE() - val extraField = ByteArray(extraFieldLength.toInt()) - val fileCommentLength = input.readUShortLE() - var fileComment = "" - val diskNumber = input.readUShortLE() - val internalAttributes = input.readUShortLE() - val externalAttributes = input.readUIntLE() - val localHeaderOffset = input.readUIntLE() - - val variableFieldsLength = - fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt() - - if (variableFieldsLength > 0) { - val fileNameBytes = ByteArray(fileNameLength.toInt()) - input.readFully(fileNameBytes) - fileName = fileNameBytes.toString(Charsets.UTF_8) - - input.readFully(extraField) - - val fileCommentBytes = ByteArray(fileCommentLength.toInt()) - input.readFully(fileCommentBytes) - fileComment = fileCommentBytes.toString(Charsets.UTF_8) - } - - flags = (flags and 0b1000u.inv() - .toUShort()) //disable data descriptor flag as they are not used - - return ZipEntry( - version, - versionNeeded, - flags, - compression, - modificationTime, - modificationDate, - crc32, - compressedSize, - uncompressedSize, - diskNumber, - internalAttributes, - externalAttributes, - localHeaderOffset, - fileName, - extraField, - fileComment, - ) - } - } - - fun readLocalExtra(buffer: ByteBuffer) { - buffer.order(ByteOrder.LITTLE_ENDIAN) - localExtraField = ByteArray(buffer.getUShort().toInt()) - } - - fun toLFH(): ByteBuffer { - val nameBytes = fileName.toByteArray(Charsets.UTF_8) - - val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size) - .also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(LFH_SIGNATURE) - buffer.putUShort(versionNeeded) - buffer.putUShort(flags) - buffer.putUShort(compression) - buffer.putUShort(modificationTime) - buffer.putUShort(modificationDate) - buffer.putUInt(crc32) - buffer.putUInt(compressedSize) - buffer.putUInt(uncompressedSize) - buffer.putUShort(nameBytes.size.toUShort()) - buffer.putUShort(localExtraField.size.toUShort()) - - buffer.put(nameBytes) - buffer.put(localExtraField) - - buffer.flip() - return buffer - } - - fun toCDE(): ByteBuffer { - val nameBytes = fileName.toByteArray(Charsets.UTF_8) - val commentBytes = fileComment.toByteArray(Charsets.UTF_8) - - val buffer = - ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size) - .also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(CDE_SIGNATURE) - buffer.putUShort(version) - buffer.putUShort(versionNeeded) - buffer.putUShort(flags) - buffer.putUShort(compression) - buffer.putUShort(modificationTime) - buffer.putUShort(modificationDate) - buffer.putUInt(crc32) - buffer.putUInt(compressedSize) - buffer.putUInt(uncompressedSize) - buffer.putUShort(nameBytes.size.toUShort()) - buffer.putUShort(extraField.size.toUShort()) - buffer.putUShort(commentBytes.size.toUShort()) - buffer.putUShort(diskNumber) - buffer.putUShort(internalAttributes) - buffer.putUInt(externalAttributes) - buffer.putUInt(localHeaderOffset) - - buffer.put(nameBytes) - buffer.put(extraField) - buffer.put(commentBytes) - - buffer.flip() - return buffer - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt b/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt deleted file mode 100644 index f864490..0000000 --- a/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt +++ /dev/null @@ -1,92 +0,0 @@ -package app.revanced.utils.signing - -import com.android.apksig.ApkSigner -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo -import org.bouncycastle.cert.X509v3CertificateBuilder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.operator.ContentSigner -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.math.BigInteger -import java.security.* -import java.security.cert.X509Certificate -import java.util.* -import java.util.logging.Logger - -internal class ApkSigner( - private val signingOptions: SigningOptions -) { - private val logger = Logger.getLogger(ApkSigner::class.java.name) - - private val signer: ApkSigner.Builder - private val passwordCharArray = signingOptions.password.toCharArray() - - init { - Security.addProvider(BouncyCastleProvider()) - - val keyStore = KeyStore.getInstance("BKS", "BC") - val alias = keyStore.let { store -> - FileInputStream(File(signingOptions.keyStoreFilePath).also { - if (!it.exists()) { - logger.info("Creating keystore at ${it.absolutePath}") - newKeystore(it) - } else { - logger.info("Using keystore at ${it.absolutePath}") - } - }).use { fis -> store.load(fis, null) } - store.aliases().nextElement() - } - - with( - ApkSigner.SignerConfig.Builder( - signingOptions.cn, - keyStore.getKey(alias, passwordCharArray) as PrivateKey, - listOf(keyStore.getCertificate(alias) as X509Certificate) - ).build() - ) { - this@ApkSigner.signer = ApkSigner.Builder(listOf(this)) - signer.setCreatedBy(signingOptions.cn) - } - } - - private fun newKeystore(out: File) { - val (publicKey, privateKey) = createKey() - val privateKS = KeyStore.getInstance("BKS", "BC") - privateKS.load(null, passwordCharArray) - privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey)) - privateKS.store(FileOutputStream(out), passwordCharArray) - } - - private fun createKey(): Pair<X509Certificate, PrivateKey> { - val gen = KeyPairGenerator.getInstance("RSA") - gen.initialize(2048) - val pair = gen.generateKeyPair() - var serialNumber: BigInteger - do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO) - val x500Name = X500Name("CN=${signingOptions.cn}") - val builder = X509v3CertificateBuilder( - x500Name, - serialNumber, - Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), - Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), - Locale.ENGLISH, - x500Name, - SubjectPublicKeyInfo.getInstance(pair.public.encoded) - ) - val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private) - return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private - } - - fun signApk(input: File, output: File): File { - signer.setInputApk(input) - signer.setOutputApk(output) - - signer.build().sign() - - return output - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt b/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt deleted file mode 100644 index 9ffdc6d..0000000 --- a/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.revanced.utils.signing - -data class SigningOptions( - val cn: String, - val password: String, - val keyStoreFilePath: String -) \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt b/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt deleted file mode 100644 index 9abfca5..0000000 --- a/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package app.revanced.patcher.options - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.data.Context -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.OptionsContainer -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchOption -import app.revanced.utils.Options -import app.revanced.utils.Options.setOptions -import org.junit.jupiter.api.MethodOrderer -import org.junit.jupiter.api.Order -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestMethodOrder - -class PatchOptionsTestPatch : BytecodePatch() { - override fun execute(context: BytecodeContext) { - // Do nothing - } - - companion object : OptionsContainer() { - var key1 by option( - PatchOption.StringOption( - "key1", null, "title1", "description1" - ) - ) - - var key2 by option( - PatchOption.BooleanOption( - "key2", true, "title2", "description2" - ) - ) - } -} - -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -internal object PatchOptionOptionsTest { - private var patches = listOf(PatchOptionsTestPatch::class.java as Class<out Patch<Context<*>>>) - - @Test - @Order(1) - fun serializeTest() { - assert(SERIALIZED_JSON == Options.serialize(patches)) - } - - @Test - @Order(2) - fun loadOptionsTest() { - patches.setOptions(CHANGED_JSON) - - assert(PatchOptionsTestPatch.key1 == "test") - assert(PatchOptionsTestPatch.key2 == false) - } - - private const val SERIALIZED_JSON = - "[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":null},{\"key\":\"key2\",\"value\":true}]}]" - - private const val CHANGED_JSON = - "[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]" -} \ No newline at end of file