diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afb223c..0861dc6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,9 +33,10 @@ jobs: build node_modules key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }} - - name: Build with Gradle + - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Cleaning is necessary to avoid uploading two identical artifacts with different versions run: ./gradlew clean --no-daemon - name: Setup semantic-release run: npm install diff --git a/.releaserc b/.releaserc index 8c4d33c..85c3f70 100644 --- a/.releaserc +++ b/.releaserc @@ -10,7 +10,7 @@ [ "@semantic-release/commit-analyzer", { "releaseRules": [ - { "type": "build", "scope": "revanced-patcher", "release": "patch" } + { "type": "build", "scope": "Needs bump", "release": "patch" } ] } ], diff --git a/CHANGELOG.md b/CHANGELOG.md index c85dad1..c0d657d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,118 @@ +# [3.0.0-dev.10](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.9...v3.0.0-dev.10) (2023-08-25) + + +### Bug Fixes + +* filtration of patches malfunctioning ([2d5a7fd](https://github.com/ReVanced/revanced-cli/commit/2d5a7fdf1eb2e13f5013a790b03f09851b167fe0)) + +# [3.0.0-dev.9](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.8...v3.0.0-dev.9) (2023-08-25) + + +### Features + +* Check for missing integrations ([c93186f](https://github.com/ReVanced/revanced-cli/commit/c93186fb9700907e65f33442e88073783cc163de)) + +# [3.0.0-dev.8](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.7...v3.0.0-dev.8) (2023-08-24) + + +### Bug Fixes + +* do not delete output file ([0f3e090](https://github.com/ReVanced/revanced-cli/commit/0f3e090418771e951dfd15e5c193421f72cbe459)) + +# [3.0.0-dev.7](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.6...v3.0.0-dev.7) (2023-08-24) + + +### Bug Fixes + +* print stack trace when a patch failed ([924c1f8](https://github.com/ReVanced/revanced-cli/commit/924c1f80ec0d17a3bdc07a0fb2015e44c49162e4)) + +# [3.0.0-dev.6](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.5...v3.0.0-dev.6) (2023-08-24) + +# [3.0.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.4...v3.0.0-dev.5) (2023-08-24) + + +### Bug Fixes + +* also delete temporary files when uninstalling ([52c3be2](https://github.com/ReVanced/revanced-cli/commit/52c3be23f2915dccaee7f9941413c8f81e14acc8)) +* delete temporary files after root installation ([a3d8705](https://github.com/ReVanced/revanced-cli/commit/a3d8705e89732a0dd4f51de28c405b6af13c8633)) +* fix running commands not running ([2c7fcaf](https://github.com/ReVanced/revanced-cli/commit/2c7fcaf4add65a12052afc5bef779dbc73debd69)) +* only check once for patch options ([11c3a6c](https://github.com/ReVanced/revanced-cli/commit/11c3a6cfd4fe59ba5d703358634a1853e1cc22a5)) + + +### Features + +* add install command ([0350b7f](https://github.com/ReVanced/revanced-cli/commit/0350b7f1a276d9dc795b22442ba4f202855ea090)) +* use friendly descriptions ([3dd875d](https://github.com/ReVanced/revanced-cli/commit/3dd875d14cca488ade6d21bbd4cce0d481692134)) + +# [3.0.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.3...v3.0.0-dev.4) (2023-08-24) + + +### Features + +* properly make use of logging facade ([41898d7](https://github.com/ReVanced/revanced-cli/commit/41898d7547690e3130372414515c5645e5dc2634)) + +# [3.0.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.2...v3.0.0-dev.3) (2023-08-23) + +# [3.0.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2023-08-23) + + +### Bug Fixes + +* specify correct class containing entry-point ([1fcc591](https://github.com/ReVanced/revanced-cli/commit/1fcc591222ab67112f2b78174a8b94106846838c)) + +# [3.0.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.5...v3.0.0-dev.1) (2023-08-23) + + +### Bug Fixes + +* do not use absolute path from custom AAPT2 binary option ([a9c2a5f](https://github.com/ReVanced/revanced-cli/commit/a9c2a5f096627dbbf8ab1b8da26fb14529ce6bc3)) +* use correct option name ([f8972ea](https://github.com/ReVanced/revanced-cli/commit/f8972eac3e5ee0a4a186c12cbe711925656d657b)) + + +* refactor!: restructure code ([07da528](https://github.com/ReVanced/revanced-cli/commit/07da528ce2223582f84bf64d2fec69714c647ddc)) + + +### Features + +* add options command ([9edbbf3](https://github.com/ReVanced/revanced-cli/commit/9edbbf31635603f89fc7bc5dcc6c023d4cdbb5a6)) +* use better logging text ([b0e748d](https://github.com/ReVanced/revanced-cli/commit/b0e748daff527ee7f417b3069882e074896fc131)) +* use separate command to list patches ([b74213f](https://github.com/ReVanced/revanced-cli/commit/b74213f66e0d04d3a0ae6197d069631388e06580)) +* use separate command to patch ([32da961](https://github.com/ReVanced/revanced-cli/commit/32da961d57537e99b39fd92b625a1c73f8314bc6)) +* use separate command to uninstall ([c0cc909](https://github.com/ReVanced/revanced-cli/commit/c0cc90962646cfffd5e2730ae556423271a7990b)) +* use simpler log ([ba758f0](https://github.com/ReVanced/revanced-cli/commit/ba758f00f4ce18791439b7e72fe1ad2e7f11f8af)) + + +### BREAKING CHANGES + +* This introduces major changes to how ReVanced CLI is used from the command line. + +# [2.23.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.4...v2.23.0-dev.5) (2023-08-14) + +# [2.23.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.3...v2.23.0-dev.4) (2023-08-13) + + +### Features + +* show full package name when listing patches ([#240](https://github.com/ReVanced/revanced-cli/issues/240)) ([7174364](https://github.com/ReVanced/revanced-cli/commit/7174364ef8ef5d6ce8351a8340f9c1a5b58eac3c)) + +# [2.23.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.2...v2.23.0-dev.3) (2023-08-03) + +# [2.23.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.1...v2.23.0-dev.2) (2023-08-03) + +# [2.23.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.22.1-dev.1...v2.23.0-dev.1) (2023-07-30) + + +### Features + +* Improve command line argument descriptions ([f9cf7d2](https://github.com/ReVanced/revanced-cli/commit/f9cf7d21b7f1c2f11234d604a1047b9d2b165f83)) + +## [2.22.1-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.22.0...v2.22.1-dev.1) (2023-07-24) + + +### Bug Fixes + +* print original instead of kebab cased names ([5eaad33](https://github.com/ReVanced/revanced-cli/commit/5eaad33dc1fbd24c36e1498f04e21d068e85f53e)) + # [2.22.0](https://github.com/revanced/revanced-cli/compare/v2.21.5...v2.22.0) (2023-07-11) diff --git a/README.md b/README.md index 0810591..0b6f6f6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # 💻 ReVanced CLI -Command line application as an alternative to the ReVanced Manager. +Command line application to use ReVanced. diff --git a/build.gradle.kts b/build.gradle.kts index 54ebe59..9c6b335 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,42 +1,23 @@ plugins { kotlin("jvm") version "1.8.20" - id("com.github.johnrengelman.shadow") version "7.1.2" + alias(libs.plugins.shadow) } group = "app.revanced" -val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR") -val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN") - -repositories { - mavenCentral() - mavenLocal() - maven { - url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") - credentials { - username = githubUsername - password = githubPassword - } - } - maven { url = uri("https://jitpack.io") } - google() -} - dependencies { - implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC") - - implementation("app.revanced:revanced-patcher:11.0.3") - implementation("info.picocli:picocli:4.7.1") - implementation("com.github.revanced:jadb:2531a28109") // updated fork - implementation("com.android.tools.build:apksig:8.1.0-alpha09") - implementation("org.bouncycastle:bcpkix-jdk15on:1.70") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.+") - testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC") + implementation(libs.revanced.patcher) + implementation(libs.kotlin.reflect) + 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) } -kotlin { - jvmToolchain(11) -} +kotlin { jvmToolchain(11) } tasks { test { @@ -45,12 +26,14 @@ tasks { events("PASSED", "SKIPPED", "FAILED") } } - build { - dependsOn(shadowJar) + + processResources { + expand("projectVersion" to project.version) } + shadowJar { manifest { - attributes("Main-Class" to "app.revanced.cli.main.MainKt") + attributes("Main-Class" to "app.revanced.cli.command.MainCommandKt") } minimize { exclude(dependency("org.jetbrains.kotlin:.*")) @@ -58,6 +41,11 @@ tasks { exclude(dependency("app.revanced:.*")) } } + + build { + 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 diff --git a/docs/0_prerequisites.md b/docs/0_prerequisites.md index bc88231..ed5660f 100644 --- a/docs/0_prerequisites.md +++ b/docs/0_prerequisites.md @@ -1,15 +1,17 @@ # 💼 Prerequisites -To use ReVanced CLI, you will need to fulfill certain requirements. +To use ReVanced CLI, you will need to fulfil specific requirements. ## 🤝 Requirements -- Java SDK 11 (Azul JDK or OpenJDK) -- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to deploy the patched APK file on your device +- Java SDK 11 (Azul Zulu JDK or OpenJDK) +- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to install the patched APK file on your device - An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7) +- ReVanced Patches +- ReVanced Integrations, if the patches require it ## ⏭️ Whats next -The next section will show, how to use [ReVanced CLI](https://github.com/revanced/revanced-cli). +The following section will show you how to use ReVanced CLI. Continue: [🛠️ Using ReVanced CLI](1_usage.md) diff --git a/docs/1_usage.md b/docs/1_usage.md index a3226de..3f30c71 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -10,13 +10,14 @@ Learn how to ReVanced CLI. adb shell exit ``` - If you want to deploy the patched APK file on your device by mounting it on top of the original APK file, you will need root access. This is optional. + Optionally, you can install the patched APK file on your device by mounting it on top of the original APK file. + You will need root permissions for this. Check if you have root permissions by running the following command: ```bash adb shell su -c exit ``` -2. Get the name of your device +2. Get your device's serial ```bash adb devices @@ -30,47 +31,81 @@ Learn how to ReVanced CLI. java -jar revanced-cli.jar -h ``` -- ### 📃 List all available patches from supplied patch bundles +- ### 📃 List patches from supplied patch bundles ```bash - java -jar revanced-cli.jar - -b revanced-patches.jar \ - -l # Names of all patches will be in kebab-case + java -jar revanced-cli.jar list-patches \ + --with-packages \ + --with-versions \ + --with-options \ + revanced-patches.jar [ ...] ``` -- ### 💉 Use ReVanced CLI to patch an APK file but deploy without root permissions +- ### ⚙️ Generate options from patches using ReVanced CLI - This will deploy the patched APK file on your device by installing it. + This will generate an `options.json` file for the patches from a list of supplied patch bundles. + The file can be supplied to ReVanced CLI later on. + +- ```bash + java -jar revanced-cli.jar options \ + --path options.json \ + --overwrite \ + revanced-patches.jar [ ...] + ``` + + > **Note**: A default `options.json` file will be automatically generated, if it does not exist + without any need for intervention when using the `patch` command. + +- ### 💉 Use ReVanced CLI to patch an APK file but install without root permissions + + This will install the patched APK file regularly on your device. ```bash - java -jar revanced-cli.jar \ - -a input.apk \ - -o patched-output.apk \ - -b revanced-patches.jar \ - -d device-name + java -jar revanced-cli.jar patch \ + --patch-bundle revanced-patches.jar \ + --out output.apk \ + --device-serial \ + input.apk ``` -- ### 👾 Use ReVanced CLI to patch an APK file but deploy with root permissions +- ### 👾 Use ReVanced CLI to patch an APK file but install with root permissions - This will deploy the patched APK file on your device by mounting it on top of the original APK file. + This will install the patched APK file on your device by mounting it on top of the original APK file. ```bash adb install input.apk - java -jar revanced-cli.jar \ - -a input.apk \ - -o patched-output.apk \ - -b revanced-patches.jar \ - -e vanced-microg-support \ - -d device-name \ - --mount + java -jar revanced-cli.jar patch \ + --patch-bundle revanced-patches.jar \ + --include some-other-patch \ + --exclude some-patch \ + --out patched-output.apk \ + --device-serial \ + --mount \ + input.apk ``` - > **Note**: Some patches from [ReVanced Patches](https://github.com/revanced/revanced-patches) also require [ReVanced Integrations](https://github.com/revanced/revanced-integrations). Supply them with the option `-m`. ReVanced Patcher will merge ReVanced Integrations automatically, depending on if the supplied patches require them. + > **Note**: Some patches may require integrations + such as [ReVanced Integrations](https://github.com/revanced/revanced-integrations). + Supply them with the option `--merge`. If any patches accepted by ReVanced Patcher require ReVanced Integrations, + they will be merged into the APK file automatically. -- ### ⚙️ Supply options to patches using ReVanced CLI +- ### 🗑️ Uninstall a patched APK file + ```bash + java -jar revanced-cli.jar utility uninstall \ + --package-name \ + + ``` - Some patches provide options. Currently, ReVanced CLI will generate and consume an `options.json` file at the location that is specified in `-o`. If the option is not specified, the options file will be generated in the current working directory. - - The options file contains all options from supplied patch bundles. + > **Note**: You can unmount an APK file + with the option `--unmount`. - > **Note**: The `options.json` file will be generated at the first time you use ReVanced CLI to patch an APK file for now. This will be changed in the future. +- ### ️ ⚙️ Manually install an APK file + + ```bash + java -jar revanced-cli.jar utility install \ + -a input.apk \ + + ``` + + > **Note**: You can mount an APK file + by supplying the package name of the app to mount the supplied APK file to over the option `--mount`. diff --git a/gradle.properties b/gradle.properties index b61decf..7fe5f91 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 2.22.0 +version = 3.0.0-dev.10 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..bae08e2 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,25 @@ +[versions] +shadow = "8.1.1" +apksig = "8.1.0" +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" +picocli = "4.7.3" +revanced-patcher = "14.1.0" + +[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" } + +[plugins] +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 028b7bc..9bd8dc7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,23 @@ +val githubUsername: String = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") +val githubPassword: String = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + mavenLocal() + google() + maven { url = uri("https://jitpack.io") } + listOf("revanced-patcher", "jadb").forEach { repo -> + maven { + url = uri("https://maven.pkg.github.com/revanced/$repo") + credentials { + username = githubUsername + password = githubPassword + } + } + } + } +} + rootProject.name = "revanced-cli" \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt b/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt deleted file mode 100644 index d67c2ad..0000000 --- a/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt +++ /dev/null @@ -1,37 +0,0 @@ -package app.revanced.cli.aligning - -import app.revanced.cli.command.MainCommand.logger -import app.revanced.patcher.PatcherResult -import app.revanced.utils.signing.align.ZipAligner -import app.revanced.utils.signing.align.zip.ZipFile -import app.revanced.utils.signing.align.zip.structures.ZipEntry -import java.io.File - -object Aligning { - fun align(result: PatcherResult, inputFile: File, outputFile: File) { - logger.info("Aligning ${inputFile.name} to ${outputFile.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 - ) - } - - file.copyEntriesFromFileAligned( - ZipFile(inputFile), - ZipAligner::getEntryAlignment - ) - } - } -} diff --git a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt new file mode 100644 index 0000000..28419d9 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt @@ -0,0 +1,91 @@ +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 picocli.CommandLine.* +import picocli.CommandLine.Help.Visibility.ALWAYS +import java.io.File +import java.util.logging.Logger + + +@Command(name = "list-patches", description = ["List patches from supplied patch bundles"]) +internal object ListPatchesCommand : Runnable { + private val logger = Logger.getLogger(ListPatchesCommand::class.java.name) + + @Parameters( + description = ["Paths to patch bundles"], arity = "1..*" + ) + private lateinit var patchBundles: Array + + @Option( + names = ["-d", "--with-descriptions"], description = ["List their descriptions"], showDefaultValue = ALWAYS + ) + private var withDescriptions: Boolean = true + + @Option( + names = ["-p", "--with-packages"], + description = ["List the packages the patches are compatible with"], + showDefaultValue = ALWAYS + ) + private var withPackages: Boolean = false + + @Option( + names = ["-v", "--with-versions"], + description = ["List the versions of the apps the patches are compatible with"], + showDefaultValue = ALWAYS + ) + private var withVersions: Boolean = false + + @Option( + names = ["-o", "--with-options"], description = ["List the options of the patches"], showDefaultValue = ALWAYS + ) + private var withOptions: Boolean = false + + override fun run() { + fun Package.buildString() = buildString { + if (withVersions && versions.isNotEmpty()) { + appendLine("Package name: $name") + appendLine("Compatible versions:") + append(versions.joinToString("\n") { version -> version }.prependIndent("\t")) + } else append("Package name: $name") + } + + fun PatchOption<*>.buildString() = buildString { + appendLine("Title: $title") + appendLine("Description: $description") + + value?.let { + appendLine("Key: $key") + append("Value: $it") + } ?: append("Key: $key") + } + + fun PatchClass.buildString() = buildString { + append("Name: $patchName") + + if (withDescriptions) append("\nDescription: $description") + + if (withOptions && options != null) { + appendLine("\nOptions:") + append( + options!!.joinToString("\n\n") { option -> option.buildString() }.prependIndent("\t") + ) + } + + if (withPackages && compatiblePackages != null) { + appendLine("\nCompatible packages:") + append( + compatiblePackages!!.joinToString("\n") { it.buildString() }.prependIndent("\t") + ) + } + } + + logger.info(PatchBundleLoader.Jar(*patchBundles).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 1da0bfd..99ac1b1 100644 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -1,265 +1,67 @@ package app.revanced.cli.command -import app.revanced.cli.aligning.Aligning -import app.revanced.cli.logging.impl.DefaultCliLogger -import app.revanced.cli.patcher.Patcher -import app.revanced.cli.patcher.logging.impl.PatcherLogger -import app.revanced.cli.signing.Signing -import app.revanced.cli.signing.SigningOptions -import app.revanced.patcher.PatcherOptions -import app.revanced.patcher.data.Context -import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages -import app.revanced.patcher.extensions.PatchExtensions.description -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.util.patch.PatchBundle -import app.revanced.utils.Options -import app.revanced.utils.Options.setOptions -import app.revanced.utils.adb.Adb -import picocli.CommandLine.* -import java.io.File -import java.nio.file.Files +import app.revanced.cli.command.utility.UtilityCommand +import app.revanced.patcher.patch.PatchClass +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.IVersionProvider +import java.util.* +import java.util.logging.* -/** - * Alias for return type of [PatchBundle.loadPatches]. - */ -internal typealias PatchList = List>> -private class CLIVersionProvider : IVersionProvider { - override fun getVersion() = arrayOf( - MainCommand::class.java.`package`.implementationVersion ?: "unknown" - ) +fun main(args: Array) { + 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) + } + + CommandLine(MainCommand).execute(*args) +} + +internal typealias PatchList = List + +private object CLIVersionProvider : IVersionProvider { + override fun getVersion(): Array { + Properties().apply { + load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties")) + }.let { + return arrayOf("ReVanced CLI v${it.getProperty("version")}") + } + } } @Command( - name = "ReVanced-CLI", + name = "revanced-cli", + description = ["Command line application to use ReVanced"], mixinStandardHelpOptions = true, - versionProvider = CLIVersionProvider::class + versionProvider = CLIVersionProvider::class, + subcommands = [ + ListPatchesCommand::class, + PatchCommand::class, + OptionsCommand::class, + UtilityCommand::class, + ] ) -internal object MainCommand : Runnable { - val logger = DefaultCliLogger() - - @ArgGroup(exclusive = false, multiplicity = "1") - lateinit var args: Args - - class Args { - // TODO: Move this so it is not required when listing patches - @Option(names = ["-a", "--apk"], description = ["Input APK file to be patched"], required = true) - lateinit var inputFile: File - - @Option(names = ["--uninstall"], description = ["Uninstall the mount variant"]) - var uninstall: Boolean = false - - @Option( - names = ["-d", "--deploy-on"], - description = ["If specified, deploy to device over ADB with given name"] - ) - var deploy: String? = null - - @ArgGroup(exclusive = false) - var patchArgs: PatchArgs? = null - } - - class PatchArgs { - @Option(names = ["-b", "--bundle"], description = ["One or more bundles of patches"], required = true) - var patchBundles = arrayOf() - - @Option(names = ["--options"], description = ["Path to patch options JSON file"]) - var optionsFile: File = File("options.json") - - @ArgGroup(exclusive = false) - var listingArgs: ListingArgs? = null - - @ArgGroup(exclusive = false) - var patchingArgs: PatchingArgs? = null - } - - class ListingArgs { - @Option(names = ["-l", "--list"], description = ["List patches"], required = true) - var listOnly: Boolean = false - - @Option(names = ["--with-versions"], description = ["List patches with compatible versions"]) - var withVersions: Boolean = false - - @Option(names = ["--with-packages"], description = ["List patches with compatible packages"]) - var withPackages: Boolean = false - } - - class PatchingArgs { - @Option(names = ["-o", "--out"], description = ["Output file path"], required = true) - lateinit var outputPath: String - - @Option(names = ["-e", "--exclude"], description = ["Explicitly exclude patches"]) - var excludedPatches = arrayOf() - - @Option( - names = ["--exclusive"], - description = ["Only installs the patches you include, not including any patch by default"] - ) - var exclusive = false - - @Option(names = ["-i", "--include"], description = ["Include patches"]) - var includedPatches = arrayOf() - - @Option(names = ["--experimental"], description = ["Disable patch version compatibility patch"]) - var experimental: Boolean = false - - @Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"]) - var mergeFiles = listOf() - - @Option(names = ["--mount"], description = ["If specified, instead of installing, mount"]) - var mount: Boolean = false - - @Option(names = ["--cn"], description = ["Overwrite the default CN for the signed file"]) - var cn = "ReVanced" - - @Option(names = ["--keystore"], description = ["File path to your keystore"]) - var keystorePath: String? = null - - @Option(names = ["-p", "--password"], description = ["Overwrite the default password for the signed file"]) - var password = "ReVanced" - - @Option(names = ["-t", "--temp-dir"], description = ["Temporary resource cache directory"]) - var cacheDirectory = "revanced-cache" - - @Option( - names = ["-c", "--clean"], - description = ["Clean the temporary resource cache directory. This will be done anyways when running the patcher"] - ) - var clean: Boolean = false - - @Option(names = ["--custom-aapt2-binary"], description = ["Path to custom aapt2 binary"]) - var aaptPath: String = "" - } - - override fun run() { - if (args.patchArgs?.listingArgs?.listOnly == true) return printListOfPatches() - if (args.uninstall) return uninstall() - - val pArgs = this.args.patchArgs?.patchingArgs ?: return - val outputFile = File(pArgs.outputPath) // the file to write to - - val allPatches = args.patchArgs!!.patchBundles.flatMap { bundle -> - PatchBundle.Jar(bundle).loadPatches() - } - - args.patchArgs!!.optionsFile.let { - if (it.exists()) allPatches.setOptions(it, logger) - else Options.serialize(allPatches, prettyPrint = true).let(it::writeText) - } - - val patcher = app.revanced.patcher.Patcher( - PatcherOptions( - args.inputFile.also { if (!it.exists()) return logger.error("Input file ${args.inputFile} does not exist.") }, - pArgs.cacheDirectory, - pArgs.aaptPath, - pArgs.cacheDirectory, - PatcherLogger - ) - ) - - // prepare adb - val adb: Adb? = args.deploy?.let { - Adb(outputFile, patcher.context.packageMetadata.packageName, args.deploy!!, !pArgs.mount) - } - - // start the patcher - val result = Patcher.start(patcher, allPatches) - - val cacheDirectory = File(pArgs.cacheDirectory) - - // align the file - val alignedFile = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_aligned.apk") - Aligning.align(result, args.inputFile, alignedFile) - - // sign the file - val finalFile = if (!pArgs.mount) { - val signedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_signed.apk") - Signing.sign( - alignedFile, - signedOutput, - SigningOptions( - pArgs.cn, - pArgs.password, - pArgs.keystorePath ?: outputFile.absoluteFile.parentFile - .resolve("${outputFile.nameWithoutExtension}.keystore") - .canonicalPath - ) - ) - - signedOutput - } else - alignedFile - - // finally copy to the specified output file - logger.info("Copying ${finalFile.name} to ${outputFile.name}") - finalFile.copyTo(outputFile, overwrite = true) - - // clean up the cache directory if needed - if (pArgs.clean) - cleanUp(pArgs.cacheDirectory) - - // deploy if specified - adb?.deploy() - - if (pArgs.clean && args.deploy != null) Files.delete(outputFile.toPath()) - - logger.info("Finished") - } - - private fun cleanUp(cacheDirectory: String) { - val result = if (File(cacheDirectory).deleteRecursively()) - "Cleaned up cache directory" - else - "Failed to clean up cache directory" - logger.info(result) - } - - private fun uninstall() { - val adb: Adb? = args.deploy?.let { - Adb( - File("placeholder_file"), - app.revanced.patcher.Patcher(PatcherOptions(args.inputFile, "")).context.packageMetadata.packageName, - args.deploy!!, - false - ) - } - adb?.uninstall() - } - - private fun printListOfPatches() { - val logged = mutableListOf() - for (patchBundlePath in args.patchArgs?.patchBundles!!) for (patch in PatchBundle.Jar(patchBundlePath) - .loadPatches()) { - if (patch.patchName in logged) continue - for (compatiblePackage in patch.compatiblePackages ?: continue) { - val packageEntryStr = buildString { - // Add package if flag is set - if (args.patchArgs?.listingArgs?.withPackages == true) { - val packageName = compatiblePackage.name.substringAfterLast(".").padStart(10) - append(packageName) - append("\t") - } - - // Add patch name - val patchName = patch.patchName.lowercase().replace(" ", "-").padStart(25) - append(patchName) - - // Add description if flag is set. - append("\t") - append(patch.description) - - // Add compatible versions, if flag is set - if (args.patchArgs?.listingArgs?.withVersions == true) { - val compatibleVersions = compatiblePackage.versions.joinToString(separator = ", ") - append("\t") - append(compatibleVersions) - } - } - - logged.add(patch.patchName) - logger.info(packageEntryStr) - } - } - } -} +private object MainCommand \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt b/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt new file mode 100644 index 0000000..91f38f2 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt @@ -0,0 +1,46 @@ +package app.revanced.cli.command + +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 +import java.util.logging.Logger + +@CommandLine.Command( + name = "options", + description = ["Generate options file from patches"], +) +internal object OptionsCommand : Runnable { + private val logger = Logger.getLogger(OptionsCommand::class.java.name) + + @CommandLine.Parameters( + description = ["Paths to patch bundles"], arity = "1..*" + ) + private lateinit var patchBundles: Array + + @CommandLine.Option( + names = ["-p", "--path"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS + ) + private var path: File = File("options.json") + + @CommandLine.Option( + names = ["-o", "--overwrite"], description = ["Overwrite existing options file"], showDefaultValue = ALWAYS + ) + private var overwrite: Boolean = false + + @CommandLine.Option( + names = ["-u", "--update"], + description = ["Update existing options by adding missing and removing non-existent options"], + showDefaultValue = ALWAYS + ) + private var update: Boolean = false + + override fun run() = if (!path.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) { + if (update) setOptions(path) + + Options.serialize(this, prettyPrint = true).let(path::writeText) + } + else logger.severe("Options file already exists, use --override to override it") +} \ 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 new file mode 100644 index 0000000..4bc2641 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -0,0 +1,342 @@ +package app.revanced.cli.command + +import app.revanced.patcher.PatchBundleLoader +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 java.io.File +import java.io.PrintWriter +import java.io.StringWriter +import java.util.logging.Logger + + +@CommandLine.Command( + name = "patch", description = ["Patch the supplied APK file with the supplied patches and integrations"] +) +internal object PatchCommand : Runnable { + private val logger = Logger.getLogger(PatchCommand::class.java.name) + + @CommandLine.Parameters( + description = ["APK file to be patched"], arity = "1..1" + ) + private lateinit var apk: File + + @CommandLine.Option( + names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true + ) + private var patchBundles = emptyList() + + @CommandLine.Option( + names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"] + ) + private var integrations = listOf() + + @CommandLine.Option( + names = ["-i", "--include"], description = ["List of patches to include"] + ) + private var includedPatches = arrayOf() + + @CommandLine.Option( + names = ["-e", "--exclude"], description = ["List of patches to exclude"] + ) + private var excludedPatches = arrayOf() + + @CommandLine.Option( + names = ["--options"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS + ) + private var optionsFile: File = File("options.json") + + @CommandLine.Option( + names = ["--exclusive"], + description = ["Only include patches that are explicitly specified to be included"], + showDefaultValue = ALWAYS + ) + private var exclusive = false + + @CommandLine.Option( + names = ["-f","--force"], + description = ["Force inclusion of patches that are incompatible with the supplied APK file's version"], + showDefaultValue = ALWAYS + ) + private var force: Boolean = false + + @CommandLine.Option( + names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true + ) + private lateinit var outputFilePath: File + + @CommandLine.Option( + names = ["-d", "--device-serial"], description = ["ADB device serial to install to"], showDefaultValue = ALWAYS + ) + private var deviceSerial: String? = null + + @CommandLine.Option( + names = ["--mount"], description = ["Install by mounting the patched APK file"], showDefaultValue = ALWAYS + ) + private var mount: Boolean = false + + @CommandLine.Option( + names = ["--common-name"], + description = ["The common name of the signer of the patched APK file"], + showDefaultValue = ALWAYS + + ) + private var commonName = "ReVanced" + + @CommandLine.Option( + names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"] + ) + private var keystorePath: String? = null + + @CommandLine.Option( + names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"] + ) + private var password = "ReVanced" + + @CommandLine.Option( + names = ["-r", "--resource-cache"], + description = ["Path to temporary resource cache directory"], + showDefaultValue = ALWAYS + ) + 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("") + + @CommandLine.Option( + names = ["-p", "--purge"], + description = ["Purge the temporary resource cache directory after patching"], + showDefaultValue = ALWAYS + ) + private var purge: Boolean = false + + 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 + + // 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) + } + + // endregion + + // region Patch + + val patcher = Patcher( + PatcherOptions( + apk, + resourceCachePath, + aaptBinaryPath.path, + resourceCachePath.absolutePath, + ) + ) + + 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") + } + } + }.get() + + patcher.close() + + // endregion + + // region Finish + + val alignAndSignedFile = sign( + apk.newAlignedFile( + result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk") + ) + ) + + logger.info("Copying to ${outputFilePath.name}") + alignAndSignedFile.copyTo(outputFilePath, overwrite = true) + + adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName)) + + 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 { + val packageName = context.packageMetadata.packageName + val packageVersion = context.packageMetadata.packageVersion + + patches.forEach patch@{ patch -> + val formattedPatchName = patch.patchName.lowercase().replace(" ", "-") + + val explicitlyExcluded = excludedPatches.contains(formattedPatchName) + if (explicitlyExcluded) return@patch logger.info("Excluding ${patch.patchName}") + + // If the patch is explicitly included, it will be included if [exclusive] is false. + val explicitlyIncluded = exclusive && includedPatches.contains(formattedPatchName) + + // If the patch is implicitly included, it will be only included if [exclusive] is false. + val implicitlyIncluded = !exclusive && patch.include + + val included = implicitlyIncluded || explicitlyIncluded + if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1. + + // At last 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 } + } + + if (!matchesVersion) return@patch logger.warning("${patch.patchName} is incompatible with version $packageVersion. " + "This patch is only compatible with version " + packages.joinToString( + ";" + ) { pkg -> + "${pkg.name}: ${pkg.versions.joinToString(", ")}" + }) + + } ?: return@patch logger.fine("${patch.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("Adding $formattedPatchName") + + 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" + logger.info(result) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt new file mode 100644 index 0000000..593680d --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt @@ -0,0 +1,42 @@ +package app.revanced.cli.command.utility + +import app.revanced.utils.adb.AdbManager +import picocli.CommandLine.* +import java.io.File +import java.util.logging.Logger + + +@Command( + name = "install", description = ["Install an APK file to devices with the supplied ADB device serials"] +) +internal object InstallCommand : Runnable { + private val logger = Logger.getLogger(InstallCommand::class.java.name) + + @Parameters( + description = ["ADB device serials"], arity = "1..*" + ) + private lateinit var deviceSerials: Array + + @Option( + names = ["-a", "--apk"], description = ["APK file to be installed"], required = true + ) + private lateinit var apk: File + + @Option( + names = ["-m", "--mount"], + description = ["Mount the supplied APK file over the app with the supplied package name"], + ) + 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)) + } + } 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 new file mode 100644 index 0000000..e17bec7 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt @@ -0,0 +1,40 @@ +package app.revanced.cli.command.utility + +import app.revanced.utils.adb.AdbManager +import picocli.CommandLine.* +import picocli.CommandLine.Help.Visibility.ALWAYS +import java.util.logging.Logger + + +@Command( + name = "uninstall", + description = ["Uninstall a patched app from the devices with the supplied ADB device serials"] +) +internal object UninstallCommand : Runnable { + private val logger = Logger.getLogger(UninstallCommand::class.java.name) + + @Parameters(description = ["ADB device serials"], arity = "1..*") + private lateinit var deviceSerials: Array + + @Option(names = ["-p", "--package-name"], description = ["Package name of the app to uninstall"], required = true) + private lateinit var packageName: String + + @Option( + names = ["-u", "--unmount"], + description = ["Uninstall by unmounting the patched APK file"], + showDefaultValue = ALWAYS + ) + private var unmount: Boolean = false + + override fun run() = try { + deviceSerials.forEach { deviceSerial -> + if (unmount) { + AdbManager.RootAdbManager(deviceSerial) + } else { + AdbManager.UserAdbManager(deviceSerial) + }.uninstall(packageName) + } + } 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/UtilityCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt new file mode 100644 index 0000000..70c77c6 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt @@ -0,0 +1,10 @@ +package app.revanced.cli.command.utility + +import picocli.CommandLine + +@CommandLine.Command( + name = "utility", + description = ["Commands for utility purposes"], + subcommands = [InstallCommand::class, UninstallCommand::class], +) +internal object UtilityCommand \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/logging/CliLogger.kt b/src/main/kotlin/app/revanced/cli/logging/CliLogger.kt deleted file mode 100644 index 06849b1..0000000 --- a/src/main/kotlin/app/revanced/cli/logging/CliLogger.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.revanced.cli.logging - -internal interface CliLogger { - fun error(msg: String) - fun info(msg: String) - fun trace(msg: String) - fun warn(msg: String) -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/logging/impl/DefaultCliLogger.kt b/src/main/kotlin/app/revanced/cli/logging/impl/DefaultCliLogger.kt deleted file mode 100644 index db9306a..0000000 --- a/src/main/kotlin/app/revanced/cli/logging/impl/DefaultCliLogger.kt +++ /dev/null @@ -1,30 +0,0 @@ -package app.revanced.cli.logging.impl - -import app.revanced.cli.command.MainCommand -import app.revanced.cli.logging.CliLogger -import java.util.logging.Logger -import java.util.logging.SimpleFormatter - -internal class DefaultCliLogger( - private val logger: Logger = Logger.getLogger(MainCommand::class.java.name), - private val errorLogger: Logger = Logger.getLogger(logger.name + "Err") -) : CliLogger { - - init { - logger.useParentHandlers = false - if (logger.handlers.isEmpty()) { - logger.addHandler(FlushingStreamHandler(System.out, SimpleFormatter())) - } - } - - companion object { - init { - System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n") - } - } - - override fun error(msg: String) = errorLogger.severe(msg) - override fun info(msg: String) = logger.info(msg) - override fun trace(msg: String) = logger.finest(msg) - override fun warn(msg: String) = errorLogger.warning(msg) -} diff --git a/src/main/kotlin/app/revanced/cli/logging/impl/FlushingStreamHandler.kt b/src/main/kotlin/app/revanced/cli/logging/impl/FlushingStreamHandler.kt deleted file mode 100644 index 4bfb394..0000000 --- a/src/main/kotlin/app/revanced/cli/logging/impl/FlushingStreamHandler.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.cli.logging.impl - -import java.io.OutputStream -import java.util.logging.Formatter -import java.util.logging.LogRecord -import java.util.logging.StreamHandler - -internal class FlushingStreamHandler(out: OutputStream, format: Formatter) : StreamHandler(out, format) { - override fun publish(record: LogRecord) { - super.publish(record) - flush() - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/main/Main.kt b/src/main/kotlin/app/revanced/cli/main/Main.kt deleted file mode 100644 index 3fc7b3d..0000000 --- a/src/main/kotlin/app/revanced/cli/main/Main.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.revanced.cli.main - -import app.revanced.cli.command.MainCommand -import picocli.CommandLine - -internal fun main(args: Array) { - CommandLine(MainCommand).execute(*args) -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt b/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt deleted file mode 100644 index 180703e..0000000 --- a/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.revanced.cli.patcher - -import app.revanced.cli.command.PatchList -import app.revanced.patcher.PatcherResult -import app.revanced.utils.patcher.addPatchesFiltered -import app.revanced.utils.patcher.applyPatchesVerbose -import app.revanced.utils.patcher.mergeFiles - -internal object Patcher { - internal fun start( - patcher: app.revanced.patcher.Patcher, - allPatches: PatchList - ): PatcherResult { - // merge files like necessary integrations - patcher.mergeFiles() - // add patches, but filter incompatible or excluded patches - patcher.addPatchesFiltered(allPatches) - // apply patches - patcher.applyPatchesVerbose() - - return patcher.save() - } -} diff --git a/src/main/kotlin/app/revanced/cli/patcher/logging/impl/PatcherLogger.kt b/src/main/kotlin/app/revanced/cli/patcher/logging/impl/PatcherLogger.kt deleted file mode 100644 index 11c04a7..0000000 --- a/src/main/kotlin/app/revanced/cli/patcher/logging/impl/PatcherLogger.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.cli.patcher.logging.impl - -import app.revanced.cli.logging.impl.DefaultCliLogger -import java.util.logging.Logger - -internal object PatcherLogger : app.revanced.patcher.logging.Logger{ - private val logger = DefaultCliLogger(Logger.getLogger(app.revanced.patcher.Patcher::class.java.name)) - - override fun error(msg: String) = logger.error(msg) - override fun info(msg: String) = logger.info(msg) - override fun warn(msg: String)= logger.warn(msg) - override fun trace(msg: String)= logger.trace(msg) -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/signing/Signing.kt b/src/main/kotlin/app/revanced/cli/signing/Signing.kt deleted file mode 100644 index 75b750c..0000000 --- a/src/main/kotlin/app/revanced/cli/signing/Signing.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.cli.signing - -import app.revanced.cli.command.MainCommand.logger -import app.revanced.utils.signing.Signer -import java.io.File - -object Signing { - fun sign(alignedFile: File, signedOutput: File, signingOptions: SigningOptions) { - logger.info("Signing ${alignedFile.name} to ${signedOutput.name}") - Signer(signingOptions).signApk(alignedFile, signedOutput) - } -} diff --git a/src/main/kotlin/app/revanced/utils/Options.kt b/src/main/kotlin/app/revanced/utils/Options.kt index 4e07d85..408e203 100644 --- a/src/main/kotlin/app/revanced/utils/Options.kt +++ b/src/main/kotlin/app/revanced/utils/Options.kt @@ -1,16 +1,18 @@ package app.revanced.utils import app.revanced.cli.command.PatchList -import app.revanced.cli.logging.CliLogger 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() /** @@ -53,22 +55,24 @@ internal object Options { * Sets the options for the patches in the list. * * @param json The JSON string containing the options. - * @param logger The logger to use for logging. */ - fun PatchList.setOptions(json: String, logger: CliLogger? = null) { + fun PatchList.setOptions(json: String) { filter { it.options?.any() == true }.let { patches -> if (patches.isEmpty()) return val patchOptions = deserialize(json) - patches.forEach { patch -> + patches.forEach patch@{ patch -> patchOptions.find { option -> option.patchName == patch.patchName }?.let { it.options.forEach { option -> try { patch.options?.set(option.key, option.value) - ?: logger?.warn("${patch.patchName} has no options") + ?: run{ + logger.warning("${patch.patchName} has no options") + return@patch + } } catch (e: NoSuchOptionException) { - logger?.error(e.message ?: "Unknown error") + logger.info(e.toString()) } } } @@ -80,10 +84,9 @@ internal object Options { * Sets the options for the patches in the list. * * @param file The file containing the JSON string containing the options. - * @param logger The logger to use for logging. * @see setOptions */ - fun PatchList.setOptions(file: File, logger: CliLogger? = null) = setOptions(file.readText(), logger) + fun PatchList.setOptions(file: File) = setOptions(file.readText()) /** * Data class for a patch and its [Option]s. diff --git a/src/main/kotlin/app/revanced/utils/adb/Adb.kt b/src/main/kotlin/app/revanced/utils/adb/Adb.kt deleted file mode 100644 index 46d1edb..0000000 --- a/src/main/kotlin/app/revanced/utils/adb/Adb.kt +++ /dev/null @@ -1,113 +0,0 @@ -package app.revanced.utils.adb - -import app.revanced.cli.command.MainCommand.logger -import se.vidstige.jadb.JadbConnection -import se.vidstige.jadb.JadbDevice -import se.vidstige.jadb.managers.PackageManager -import java.io.File -import java.util.concurrent.Executors - -internal class Adb( - private val file: File, - private val packageName: String, - deviceName: String, - private val modeInstall: Boolean = false, - private val logging: Boolean = true -) { - private val device: JadbDevice - - init { - device = JadbConnection().devices.let { device -> device.find { it.serial == deviceName } ?: device.first() } - ?: throw IllegalArgumentException("No such device with name $deviceName") - - if (!modeInstall && device.run("su -h", false) != 0) - throw IllegalArgumentException("Root required on $deviceName. Task failed") - } - - private fun String.replacePlaceholder(with: String? = null): String { - return this.replace(Constants.PLACEHOLDER, with ?: packageName) - } - - internal fun deploy() { - if (modeInstall) { - logger.info("Installing without mounting") - - PackageManager(device).install(file) - } else { - logger.info("Installing by mounting") - - // push patched file - device.copy(Constants.PATH_INIT_PUSH, file) - - // create revanced folder path - device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}") - - // prepare mounting the apk - device.run(Constants.COMMAND_PREPARE_MOUNT_APK.replacePlaceholder()) - - // push mount script - device.createFile( - Constants.PATH_INIT_PUSH, - Constants.CONTENT_MOUNT_SCRIPT.replacePlaceholder() - ) - // install mount script - device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder()) - - // unmount the apk for sanity - device.run(Constants.COMMAND_UMOUNT.replacePlaceholder()) - // mount the apk - device.run(Constants.PATH_MOUNT.replacePlaceholder()) - - // relaunch app - device.run(Constants.COMMAND_RESTART.replacePlaceholder()) - - // log the app - log() - } - } - - internal fun uninstall() { - logger.info("Uninstalling by unmounting") - - // unmount the apk - device.run(Constants.COMMAND_UMOUNT.replacePlaceholder()) - - // delete revanced app - device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_REVANCED_APP).replacePlaceholder()) - - // delete mount script - device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_MOUNT).replacePlaceholder()) - - logger.info("Finished uninstalling") - } - - private fun log() { - val executor = Executors.newSingleThreadExecutor() - val pipe = if (logging) { - ProcessBuilder.Redirect.INHERIT - } else { - ProcessBuilder.Redirect.PIPE - } - - val process = device.buildCommand(Constants.COMMAND_LOGCAT.replacePlaceholder()) - .redirectOutput(pipe) - .redirectError(pipe) - .useExecutor(executor) - .start() - - Thread.sleep(500) // give the app some time to start up. - while (true) { - try { - while (device.run("${Constants.COMMAND_PID_OF} $packageName") == 0) { - Thread.sleep(1000) - } - break - } catch (e: Exception) { - throw RuntimeException("An error occurred while monitoring the state of app", e) - } - } - logger.info("Stopped logging because the app was closed") - process.destroy() - executor.shutdown() - } -} diff --git a/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt new file mode 100644 index 0000000..1bd0043 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt @@ -0,0 +1,140 @@ +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 index 1b3af07..7ec7eb8 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Commands.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Commands.kt @@ -5,10 +5,9 @@ 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\'") - } + if (su) return shellProcessBuilder("su -c \'$command\'") val args = command.split(" ") as ArrayList val cmd = args.removeFirst() @@ -20,10 +19,15 @@ internal fun JadbDevice.run(command: String, su: Boolean = true): Int { return this.buildCommand(command, su).start().waitFor() } -internal fun JadbDevice.copy(targetPath: String, file: File) { - push(file, RemoteFile(targetPath)) -} +internal fun JadbDevice.hasSu() = + this.startCommand("su -h", false).waitFor() == 0 -internal fun JadbDevice.createFile(targetFile: String, content: String) { +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)) -} \ No newline at end of file + + +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 index f4aa5f2..ea5095e 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Constants.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Constants.kt @@ -1,54 +1,37 @@ package app.revanced.utils.adb internal object Constants { - // template placeholder to replace a string in commands - internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME" + internal const val PLACEHOLDER = "PLACEHOLDER" - // utility commands - private const val COMMAND_CHMOD_MOUNT = "chmod +x" - internal const val COMMAND_PID_OF = "pidof -s" - internal const val COMMAND_CREATE_DIR = "mkdir -p" - internal const val COMMAND_LOGCAT = "logcat -c && logcat | grep AndroidRuntime" - internal const val COMMAND_RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | xargs am start -n && kill ${'$'}($COMMAND_PID_OF $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" - // default mount file name - private const val NAME_MOUNT_SCRIPT = "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)" - // initial directory to push files to via adb push - internal const val PATH_INIT_PUSH = "/data/local/tmp/revanced.delete" + 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" - // revanced path - internal const val PATH_REVANCED = "/data/adb/revanced/" - - // revanced apk path - internal const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk" - - // delete command - internal const val COMMAND_DELETE = "rm -rf $PLACEHOLDER" - - // mount script path - internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT" - - // move to revanced apk path & set permissions - internal const val COMMAND_PREPARE_MOUNT_APK = - "base_path=\"$PATH_REVANCED_APP\" && mv $PATH_INIT_PUSH ${'$'}base_path && chmod 644 ${'$'}base_path && chown system:system ${'$'}base_path && chcon u:object_r:apk_data_file:s0 ${'$'}base_path" - - // unmount command - internal const val COMMAND_UMOUNT = + 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" - // install mount script & set permissions - internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT" + internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH" - // mount script - internal val CONTENT_MOUNT_SCRIPT = + 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="$PATH_REVANCED_APP" + 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 diff --git a/src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt b/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt similarity index 74% rename from src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt rename to src/main/kotlin/app/revanced/utils/align/ZipAligner.kt index be40ba1..568e20b 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt +++ b/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt @@ -1,6 +1,6 @@ -package app.revanced.utils.signing.align +package app.revanced.utils.align -import app.revanced.utils.signing.align.zip.structures.ZipEntry +import app.revanced.utils.align.zip.structures.ZipEntry internal object ZipAligner { private const val DEFAULT_ALIGNMENT = 4 diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt b/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt similarity index 96% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt rename to src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt index 87f7db6..330c689 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt @@ -1,4 +1,4 @@ -package app.revanced.utils.signing.align.zip +package app.revanced.utils.align.zip import java.io.DataInput import java.io.DataOutput diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt b/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt similarity index 75% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt rename to src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt index e64cd1a..f961488 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt @@ -1,7 +1,7 @@ -package app.revanced.utils.signing.align.zip +package app.revanced.utils.align.zip -import app.revanced.utils.signing.align.zip.structures.ZipEndRecord -import app.revanced.utils.signing.align.zip.structures.ZipEntry +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 @@ -11,15 +11,15 @@ import java.util.zip.CRC32 import java.util.zip.Deflater class ZipFile(file: File) : Closeable { - var entries: MutableList = mutableListOf() + private var entries: MutableList = mutableListOf() private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") - private var CDNeedsRewrite = false + private var centralDirectoryNeedsRewrite = false private val compressionLevel = 5 init { - //if file isn't empty try to load entries + // If file isn't empty try to load entries. if (file.length() > 0) { val endRecord = findEndRecord() @@ -29,17 +29,17 @@ class ZipFile(file: File) : Closeable { entries = readEntries(endRecord).toMutableList() } - //seek back to start for writing + // 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 + // 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 + // Possible beginning of signature. if (filePointer.readByte() == 0x50.toByte()) { - //seek back to get the full int + // Seek back to get the full int. filePointer.seek(i) val possibleSignature = filePointer.readUIntLE() if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { @@ -76,7 +76,7 @@ class ZipFile(file: File) : Closeable { } private fun writeCD() { - val CDStart = filePointer.channel.position().toUInt() + val centralDirectoryStartOffset = filePointer.channel.position().toUInt() entries.forEach { filePointer.channel.write(it.toCDE()) @@ -89,8 +89,8 @@ class ZipFile(file: File) : Closeable { 0u, entriesCount, entriesCount, - filePointer.channel.position().toUInt() - CDStart, - CDStart, + filePointer.channel.position().toUInt() - centralDirectoryStartOffset, + centralDirectoryStartOffset, "" ) @@ -98,7 +98,7 @@ class ZipFile(file: File) : Closeable { } private fun addEntry(entry: ZipEntry, data: ByteBuffer) { - CDNeedsRewrite = true + centralDirectoryNeedsRewrite = true entry.localHeaderOffset = filePointer.channel.position().toUInt() @@ -114,8 +114,7 @@ class ZipFile(file: File) : Closeable { compressor.finish() val uncompressedSize = data.size - val compressedData = - ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger + val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger. val compressedDataLength = compressor.deflate(compressedData) val compressedBuffer = @@ -126,7 +125,7 @@ class ZipFile(file: File) : Closeable { val crc = CRC32() crc.update(data) - entry.compression = 8u //deflate compression + entry.compression = 8u // Deflate compression. entry.uncompressedSize = uncompressedSize.toUInt() entry.compressedSize = compressedDataLength.toUInt() entry.crc32 = crc.value.toUInt() @@ -136,14 +135,14 @@ class ZipFile(file: File) : Closeable { private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) { alignment?.let { - //calculate where data would end up + // Calculate where data would end up. val dataOffset = filePointer.filePointer + entry.LFHSize val mod = dataOffset % alignment - //wrong alignment + // Wrong alignment. if (mod != 0L) { - //add padding at end of extra field + // Add padding at end of extra field. entry.localExtraField = entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt()) } @@ -152,7 +151,7 @@ class ZipFile(file: File) : Closeable { addEntry(entry, data) } - fun getDataForEntry(entry: ZipEntry): ByteBuffer { + private fun getDataForEntry(entry: ZipEntry): ByteBuffer { return filePointer.channel.map( FileChannel.MapMode.READ_ONLY, entry.dataOffset.toLong(), @@ -160,9 +159,15 @@ class ZipFile(file: File) : Closeable { ) } + /** + * 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 //don't add duplicates + if (entries.any { it.fileName == entry.fileName }) continue // Skip duplicates val data = file.getDataForEntry(entry) addEntryCopyData(entry, data, entryAlignment(entry)) @@ -170,7 +175,7 @@ class ZipFile(file: File) : Closeable { } override fun close() { - if (CDNeedsRewrite) writeCD() + if (centralDirectoryNeedsRewrite) writeCD() filePointer.close() } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt similarity index 89% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt rename to src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt index d26e551..387679e 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt @@ -1,9 +1,9 @@ -package app.revanced.utils.signing.align.zip.structures +package app.revanced.utils.align.zip.structures -import app.revanced.utils.signing.align.zip.putUInt -import app.revanced.utils.signing.align.zip.putUShort -import app.revanced.utils.signing.align.zip.readUIntLE -import app.revanced.utils.signing.align.zip.readUShortLE +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 diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt similarity index 98% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt rename to src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt index d99a73d..316a836 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt @@ -1,6 +1,6 @@ -package app.revanced.utils.signing.align.zip.structures +package app.revanced.utils.align.zip.structures -import app.revanced.utils.signing.align.zip.* +import app.revanced.utils.align.zip.* import java.io.DataInput import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt b/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt deleted file mode 100644 index 53eb07e..0000000 --- a/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt +++ /dev/null @@ -1,75 +0,0 @@ -package app.revanced.utils.patcher - -import app.revanced.cli.command.MainCommand.args -import app.revanced.cli.command.MainCommand.logger -import app.revanced.cli.command.PatchList -import app.revanced.patcher.Patcher -import app.revanced.patcher.data.Context -import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages -import app.revanced.patcher.extensions.PatchExtensions.include -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.patch.Patch - -fun Patcher.addPatchesFiltered(allPatches: PatchList) { - val packageName = this.context.packageMetadata.packageName - val packageVersion = this.context.packageMetadata.packageVersion - - val includedPatches = mutableListOf>>() - allPatches.forEach patchLoop@{ patch -> - val compatiblePackages = patch.compatiblePackages - val patchName = patch.patchName.lowercase().replace(" ", "-") - val args = args.patchArgs?.patchingArgs!! - - val prefix = "Skipping $patchName" - - if (compatiblePackages == null) logger.trace("$patchName: No constraint on packages.") - else { - if (!compatiblePackages.any { it.name == packageName }) { - logger.trace("$prefix: Incompatible with $packageName. This patch is only compatible with ${ - compatiblePackages.joinToString( - ", " - ) { it.name } - }") - return@patchLoop - } - - if (!(args.experimental || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion } })) { - val compatibleWith = compatiblePackages.joinToString(";") { _package -> - "${_package.name}: ${_package.versions.joinToString(", ")}" - } - logger.warn("$prefix: Incompatible with version $packageVersion. This patch is only compatible with $compatibleWith") - return@patchLoop - } - } - - if (args.excludedPatches.contains(patchName)) { - logger.info("$prefix: Manually excluded") - return@patchLoop - } else if ((!patch.include || args.exclusive) && !args.includedPatches.contains(patchName)) { - logger.info("$prefix: Excluded by default") - return@patchLoop - } - - logger.trace("Adding $patchName") - includedPatches.add(patch) - } - - this.addPatches(includedPatches) -} - -fun Patcher.applyPatchesVerbose() { - this.executePatches().forEach { (patch, result) -> - if (result.isSuccess) { - logger.info("$patch succeeded") - return@forEach - } - logger.error("$patch failed:") - result.exceptionOrNull()!!.printStackTrace() - } -} - -fun Patcher.mergeFiles() { - this.addIntegrations(args.patchArgs?.patchingArgs!!.mergeFiles) { file -> - logger.info("Merging $file") - } -} diff --git a/src/main/kotlin/app/revanced/utils/signing/Signer.kt b/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt similarity index 67% rename from src/main/kotlin/app/revanced/utils/signing/Signer.kt rename to src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt index 358395a..f864490 100644 --- a/src/main/kotlin/app/revanced/utils/signing/Signer.kt +++ b/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt @@ -1,7 +1,5 @@ package app.revanced.utils.signing -import app.revanced.cli.command.MainCommand.logger -import app.revanced.cli.signing.SigningOptions import com.android.apksig.ApkSigner import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo @@ -17,11 +15,44 @@ import java.math.BigInteger import java.security.* import java.security.cert.X509Certificate import java.util.* +import java.util.logging.Logger -internal class Signer( +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") @@ -50,30 +81,12 @@ internal class Signer( return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private } - fun signApk(input: File, output: File) { - Security.addProvider(BouncyCastleProvider()) - - // TODO: keystore should be saved securely - val ks = File(signingOptions.keyStoreFilePath) - if (!ks.exists()) newKeystore(ks) else { - logger.info("Found existing keystore: ${ks.name}") - } - - val keyStore = KeyStore.getInstance("BKS", "BC") - FileInputStream(ks).use { fis -> keyStore.load(fis, null) } - val alias = keyStore.aliases().nextElement() - - val config = ApkSigner.SignerConfig.Builder( - signingOptions.cn, - keyStore.getKey(alias, passwordCharArray) as PrivateKey, - listOf(keyStore.getCertificate(alias) as X509Certificate) - ).build() - - val signer = ApkSigner.Builder(listOf(config)) - signer.setCreatedBy(signingOptions.cn) + 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/cli/signing/SigningOptions.kt b/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt similarity index 75% rename from src/main/kotlin/app/revanced/cli/signing/SigningOptions.kt rename to src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt index 252ef65..9ffdc6d 100644 --- a/src/main/kotlin/app/revanced/cli/signing/SigningOptions.kt +++ b/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt @@ -1,4 +1,4 @@ -package app.revanced.cli.signing +package app.revanced.utils.signing data class SigningOptions( val cn: String, diff --git a/src/main/resources/app/revanced/cli/version.properties b/src/main/resources/app/revanced/cli/version.properties new file mode 100644 index 0000000..308c9f8 --- /dev/null +++ b/src/main/resources/app/revanced/cli/version.properties @@ -0,0 +1 @@ +version=${projectVersion} \ 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 index 68759ee..9abfca5 100644 --- a/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt +++ b/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt @@ -2,7 +2,10 @@ package app.revanced.patcher.options import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.Context -import app.revanced.patcher.patch.* +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 @@ -11,8 +14,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder class PatchOptionsTestPatch : BytecodePatch() { - override fun execute(context: BytecodeContext): PatchResult { - return PatchResultSuccess() + override fun execute(context: BytecodeContext) { + // Do nothing } companion object : OptionsContainer() { @@ -32,7 +35,7 @@ class PatchOptionsTestPatch : BytecodePatch() { @TestMethodOrder(MethodOrderer.OrderAnnotation::class) internal object PatchOptionOptionsTest { - private var patches = listOf(PatchOptionsTestPatch::class.java as Class>) + private var patches = listOf(PatchOptionsTestPatch::class.java as Class>>) @Test @Order(1)