From f6d60a34608cbfc00b2b2004b9d1d70c17479464 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 1 May 2022 02:07:25 +0200 Subject: [PATCH] refactor: migration to `picocli` Signed-off-by: oSumAtrIX --- build.gradle.kts | 19 +- src/main/kotlin/app/revanced/cli/Main.kt | 178 ------------------ .../kotlin/app/revanced/cli/MainCommand.kt | 110 +++++++++++ .../app/revanced/cli/runner/AdbRunner.kt | 160 ---------------- .../app/revanced/cli/utils/Preconditions.kt | 24 --- .../app/revanced/{cli => }/utils/Scripts.kt | 4 +- .../{cli/utils => utils/dex}/DexReplacer.kt | 2 +- .../{cli/utils => utils/patch}/PatchLoader.kt | 2 +- .../{cli/utils => utils/patch}/Patches.kt | 2 +- .../utils/signer => utils/signing}/KeySet.kt | 2 +- .../utils/signer => utils/signing}/Signer.kt | 4 +- 11 files changed, 123 insertions(+), 384 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/cli/Main.kt create mode 100644 src/main/kotlin/app/revanced/cli/MainCommand.kt delete mode 100644 src/main/kotlin/app/revanced/cli/runner/AdbRunner.kt delete mode 100644 src/main/kotlin/app/revanced/cli/utils/Preconditions.kt rename src/main/kotlin/app/revanced/{cli => }/utils/Scripts.kt (93%) rename src/main/kotlin/app/revanced/{cli/utils => utils/dex}/DexReplacer.kt (96%) rename src/main/kotlin/app/revanced/{cli/utils => utils/patch}/PatchLoader.kt (96%) rename src/main/kotlin/app/revanced/{cli/utils => utils/patch}/Patches.kt (94%) rename src/main/kotlin/app/revanced/{cli/utils/signer => utils/signing}/KeySet.kt (81%) rename src/main/kotlin/app/revanced/{cli/utils/signer => utils/signing}/Signer.kt (99%) diff --git a/build.gradle.kts b/build.gradle.kts index c9b9851..730003e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,35 +20,26 @@ repositories { } } -val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4" +val patchesDependency = "app.revanced:revanced-patches:+" dependencies { implementation(kotlin("stdlib")) - implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.4") - - implementation("app.revanced:revanced-patcher:1.0.0-dev.8") + implementation("app.revanced:revanced-patcher:+") implementation(patchesDependency) - - implementation("com.google.code.gson:gson:2.9.0") - implementation("me.tongfei:progressbar:0.9.3") - implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead. - implementation("org.bouncycastle:bcpkix-jdk15on:1.70") + implementation("info.picocli:picocli:+") + implementation("org.bouncycastle:bcpkix-jdk15on:+") } -val cliMainClass = "app.revanced.cli.Main" - tasks { build { dependsOn(shadowJar) } shadowJar { dependencies { - // This makes sure we link to the library, but don't include it. - // So, a "runtime only" dependency. exclude(dependency(patchesDependency)) } manifest { - attributes("Main-Class" to cliMainClass) + attributes("Main-Class" to "app.revanced.cli.Main") attributes("Implementation-Title" to project.name) attributes("Implementation-Version" to project.version) } diff --git a/src/main/kotlin/app/revanced/cli/Main.kt b/src/main/kotlin/app/revanced/cli/Main.kt deleted file mode 100644 index a598750..0000000 --- a/src/main/kotlin/app/revanced/cli/Main.kt +++ /dev/null @@ -1,178 +0,0 @@ -package app.revanced.cli - -import app.revanced.cli.runner.AdbRunner -import app.revanced.cli.utils.PatchLoader -import app.revanced.cli.utils.Patches -import app.revanced.cli.utils.Preconditions -import app.revanced.patcher.Patcher -import app.revanced.patcher.patch.PatchMetadata -import app.revanced.patcher.patch.PatchResult -import kotlinx.cli.ArgParser -import kotlinx.cli.ArgType -import kotlinx.cli.default -import kotlinx.cli.required -import me.tongfei.progressbar.ProgressBarBuilder -import me.tongfei.progressbar.ProgressBarStyle -import java.io.File -import java.nio.file.Files - -private const val CLI_NAME = "ReVanced CLI" -private val CLI_VERSION = Main::class.java.`package`.implementationVersion ?: "0.0.0-unknown" - -class Main { - companion object { - private fun runCLI( - inApk: String, - inPatches: String, - inIntegrations: String?, - inOutput: String, - inRunOnAdb: String?, - hideResults: Boolean, - noLogging: Boolean, - ) { - val bar = ProgressBarBuilder() - .setTaskName("Working..") - .setUpdateIntervalMillis(25) - .continuousUpdate() - .setStyle(ProgressBarStyle.ASCII) - .build() - .maxHint(1) - .setExtraMessage("Initializing") - val apk = Preconditions.isFile(inApk) - val patchesFile = Preconditions.isFile(inPatches) - val output = Preconditions.isDirectory(inOutput) - bar.step() - - val patcher = Patcher(apk) - - inIntegrations?.let { - bar.reset().maxHint(1) - .extraMessage = "Merging integrations" - val integrations = Preconditions.isFile(it) - patcher.addFiles(listOf(integrations)) - bar.step() - } - - bar.reset().maxHint(1) - .extraMessage = "Loading patches" - PatchLoader.injectPatches(patchesFile) - val patches = Patches.loadPatches().map { it() } - patcher.addPatches(patches) - bar.step() - - bar.reset().maxHint(1) - .extraMessage = "Resolving signatures" - patcher.resolveSignatures() - bar.step() - - val szPatches = patches.size.toLong() - bar.reset().maxHint(szPatches) - .extraMessage = "Applying patches" - val results = patcher.applyPatches { - bar.step().extraMessage = "Applying $it" - } - - bar.reset().maxHint(-1) - .extraMessage = "Generating dex files" - val dexFiles = patcher.save() - - val szDexFiles = dexFiles.size.toLong() - bar.reset().maxHint(szDexFiles) - .extraMessage = "Saving dex files" - dexFiles.forEach { (dexName, dexData) -> - Files.write(File(output, dexName).toPath(), dexData.data) - bar.step() - } - bar.stepTo(szDexFiles) - - bar.close() - - inRunOnAdb?.let { device -> - AdbRunner.runApk( - apk, - dexFiles, - output, - device, - noLogging - ) - } - - println("All done!") - if (!hideResults) { - printResults(results) - } - } - - private fun printResults(results: Map>) { - for ((metadata, result) in results) { - if (result.isSuccess) { - println("${metadata.shortName} was applied successfully!") - } else { - println("${metadata.shortName} failed to apply! Cause:") - result.exceptionOrNull()!!.printStackTrace() - } - } - } - - @JvmStatic - fun main(args: Array) { - println("$CLI_NAME version $CLI_VERSION") - val parser = ArgParser(CLI_NAME) - - // TODO: add some kind of incremental building, so merging integrations can be skipped. - // this can be achieved manually, but doing it automatically is better. - - val apk by parser.option( - ArgType.String, - fullName = "apk", - shortName = "a", - description = "APK file" - ).required() - val patches by parser.option( - ArgType.String, - fullName = "patches", - shortName = "p", - description = "Patches JAR file" - ).required() - val integrations by parser.option( - ArgType.String, - fullName = "integrations", - shortName = "i", - description = "Integrations APK file" - ) - val output by parser.option( - ArgType.String, - fullName = "output", - shortName = "o", - description = "Output directory" - ).required() - val runOnAdb by parser.option( - ArgType.String, - fullName = "run-on", - description = "After the CLI is done building, which ADB device should it run on?" - ) - // TODO: package name - val hideResults by parser.option( - ArgType.Boolean, - fullName = "hide-results", - description = "Don't print the patch results." - ).default(false) - val noLogging by parser.option( - ArgType.Boolean, - fullName = "no-logging", - description = "Don't print the output of the application when used in combination with \"run-on\"." - ).default(false) - - parser.parse(args) - runCLI( - apk, - patches, - integrations, - output, - runOnAdb, - hideResults, - noLogging, - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/MainCommand.kt b/src/main/kotlin/app/revanced/cli/MainCommand.kt new file mode 100644 index 0000000..7527e4e --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -0,0 +1,110 @@ +package app.revanced.cli + +import app.revanced.cli.MainCommand.excludedPatches +import app.revanced.cli.MainCommand.patchBundles +import app.revanced.patcher.Patcher +import app.revanced.patcher.patch.Patch +import app.revanced.utils.dex.DexReplacer +import app.revanced.utils.patch.PatchLoader +import app.revanced.utils.patch.Patches +import app.revanced.utils.signing.Signer +import picocli.CommandLine +import picocli.CommandLine.* +import java.io.File + +@Command( + name = "ReVanced-CLI", + version = ["1.0.0"], + mixinStandardHelpOptions = true +) +object MainCommand : Runnable { + @Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"]) + var patchBundles = arrayOf() + + @Parameters(paramLabel = "EXCLUDE", description = ["Which patches to exclude"]) + var excludedPatches = arrayOf() + + @Option(names = ["-l", "--list"], description = ["List patches only"]) + var listOnly: Boolean = false + + @Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"]) + var mergeFiles = listOf() + + @Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true) + lateinit var inputFile: File + + @Option(names = ["-o", "--out"], description = ["Output file path"], required = true) + lateinit var outputPath: String + + override fun run() { + if (listOnly) { + patchBundles.forEach { + PatchLoader.injectPatches(it) + Patches.loadPatches().forEach { + println(it().metadata) + } + } + return + } + + val patcher = Patcher(inputFile) + // merge files like necessary integrations + patcher.addFiles(mergeFiles) + // add patches, but filter incompatible or excluded patches + patcher.addPatchesFiltered() + // apply patches + for (patchResult in patcher.applyPatches { + println("Applying: $it") + }) { + println(patchResult) + } + + // write output file + val outFile = File(outputPath) + inputFile.copyTo(outFile) + DexReplacer.replaceDex(outFile, patcher.save()) + + // sign the apk file + Signer.signApk(outFile) + } +} + +private fun Patcher.addPatchesFiltered() { + // TODO: get package metadata (outside of this method) for apk file which needs to be patched + val packageName = "com.example.exampleApp" + val packageVersion = "1.2.3" + + patchBundles.forEach { bundle -> + PatchLoader.injectPatches(bundle) + val includedPatches = mutableListOf() + Patches.loadPatches().forEach patch@{ + val patch = it() + + // TODO: filter out incompatible patches with package metadata + val filterOutPatches = true + if (filterOutPatches && + !patch.metadata.compatiblePackages.any { packageMetadata -> + packageMetadata.name == packageName && + packageMetadata.versions.any { + it == packageVersion + } + } + ) { + // TODO: report to stdout + return@patch + } + + if (excludedPatches.contains(patch.metadata.shortName)) { + // TODO: report to stdout + return@patch + } + + includedPatches.add(patch) + } + this.addPatches(includedPatches) + } +} + +fun main(args: Array) { + CommandLine(MainCommand).execute(*args) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/runner/AdbRunner.kt b/src/main/kotlin/app/revanced/cli/runner/AdbRunner.kt deleted file mode 100644 index 99e94b9..0000000 --- a/src/main/kotlin/app/revanced/cli/runner/AdbRunner.kt +++ /dev/null @@ -1,160 +0,0 @@ -package app.revanced.cli.runner - -import app.revanced.cli.utils.DexReplacer -import app.revanced.cli.utils.Scripts -import app.revanced.cli.utils.signer.Signer -import me.tongfei.progressbar.ProgressBar -import me.tongfei.progressbar.ProgressBarBuilder -import me.tongfei.progressbar.ProgressBarStyle -import org.jf.dexlib2.writer.io.MemoryDataStore -import se.vidstige.jadb.JadbConnection -import se.vidstige.jadb.JadbDevice -import se.vidstige.jadb.RemoteFile -import se.vidstige.jadb.ShellProcessBuilder -import java.io.File -import java.util.concurrent.Executors - -object AdbRunner { - fun runApk( - apk: File, - dexFiles: Map, - outputDir: File, - deviceName: String, - noLogging: Boolean - ) { - lateinit var dvc: JadbDevice - pbar("Initializing").use { bar -> - dvc = JadbConnection().findDevice(deviceName) - ?: throw IllegalArgumentException("No such device with name $deviceName") - if (!dvc.hasSu()) - throw IllegalArgumentException("Device $deviceName is not rooted or does not have su") - bar.step() - } - - lateinit var tmpFile: File // we need this file at the end to clean up. - pbar("Generating APK file", 3).use { bar -> - bar.step().extraMessage = "Creating APK file" - tmpFile = File(outputDir, "revanced.apk") - apk.copyTo(tmpFile, true) - - bar.step().extraMessage = "Replacing dex files" - DexReplacer.replaceDex(tmpFile, dexFiles) - - bar.step().extraMessage = "Signing APK file" - try { - Signer.signApk(tmpFile) - } catch (e: SecurityException) { - throw IllegalStateException( - "A security exception occurred when signing the APK! " + - "If it has anything to with \"cannot authenticate\" then please make sure " + - "you are using Zulu or OpenJDK as they do work when using the adb runner.", - e - ) - } - } - - pbar("Running application", 6, false).use { bar -> - bar.step().extraMessage = "Pushing mount scripts" - dvc.push(Scripts.MOUNT_SCRIPT, RemoteFile(Scripts.SCRIPT_PATH)) - dvc.cmd(Scripts.CREATE_DIR_COMMAND).assertZero() - dvc.cmd(Scripts.MV_MOUNT_COMMAND).assertZero() - dvc.cmd(Scripts.CHMOD_MOUNT_COMMAND).assertZero() - - bar.step().extraMessage = "Pushing APK file" - dvc.push(tmpFile, RemoteFile(Scripts.APK_PATH)) - - bar.step().extraMessage = "Mounting APK file" - dvc.cmd(Scripts.STOP_APP_COMMAND).startAndWait() - dvc.cmd(Scripts.START_MOUNT_COMMAND).assertZero() - - bar.step().extraMessage = "Starting APK file" - dvc.cmd(Scripts.START_APP_COMMAND).assertZero() - - bar.step().setExtraMessage("Debugging APK file").refresh() - println("\nWaiting until app is closed.") - val executor = Executors.newSingleThreadExecutor() - val pipe = if (noLogging) { - ProcessBuilder.Redirect.PIPE - } else { - ProcessBuilder.Redirect.INHERIT - } - val p = dvc.cmd(Scripts.LOGCAT_COMMAND) - .redirectOutput(pipe) - .redirectError(pipe) - .useExecutor(executor) - .start() - Thread.sleep(250) // give the app some time to start up. - while (true) { - try { - while (dvc.cmd(Scripts.PIDOF_APP_COMMAND).startAndWait() == 0) { - Thread.sleep(250) - } - break - } catch (e: Exception) { - throw RuntimeException("An error occurred while monitoring state of app", e) - } - } - println("App closed, continuing.") - p.destroy() - executor.shutdown() - - bar.step().extraMessage = "Unmounting APK file" - var exitCode: Int - do { - exitCode = dvc.cmd(Scripts.UNMOUNT_COMMAND).startAndWait() - } while (exitCode != 0) - } - } -} - -private fun JadbDevice.push(s: String, remoteFile: RemoteFile) = - this.push(s.byteInputStream(), System.currentTimeMillis(), 644, remoteFile) - -private fun JadbConnection.findDevice(device: String): JadbDevice? { - return devices.find { it.serial == device } -} - -private fun JadbDevice.cmd(s: String): ShellProcessBuilder { - val args = s.split(" ") as ArrayList - val cmd = args.removeFirst() - return shellProcessBuilder(cmd, *args.toTypedArray()) -} - -private fun JadbDevice.hasSu(): Boolean { - return cmd("su -h").startAndWait() == 0 -} - -private fun ShellProcessBuilder.startAndWait(): Int { - return start().waitFor() -} - -private fun ShellProcessBuilder.assertZero() { - if (startAndWait() != 0) { - val cmd = getcmd() - throw IllegalStateException("ADB returned non-zero status code for command: $cmd") - } -} - -private fun pbar(task: String, steps: Long = 1, update: Boolean = true): ProgressBar { - val b = ProgressBarBuilder().setTaskName(task) - if (update) b - .setUpdateIntervalMillis(250) - .continuousUpdate() - return b - .setStyle(ProgressBarStyle.ASCII) - .build() - .maxHint(steps + 1) -} - -private fun ProgressBar.use(block: (ProgressBar) -> Unit) { - block(this) - stepTo(max) // step to 100% - extraMessage = "" // clear extra message - close() -} - -private fun ShellProcessBuilder.getcmd(): String { - val f = this::class.java.getDeclaredField("command") - f.isAccessible = true - return f.get(this) as String -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/utils/Preconditions.kt b/src/main/kotlin/app/revanced/cli/utils/Preconditions.kt deleted file mode 100644 index ff0da2d..0000000 --- a/src/main/kotlin/app/revanced/cli/utils/Preconditions.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.revanced.cli.utils - -import java.io.File -import java.io.FileNotFoundException - -class Preconditions { - companion object { - fun isFile(path: String): File { - val f = File(path) - if (!f.exists()) { - throw FileNotFoundException(f.toString()) - } - return f - } - - fun isDirectory(path: String): File { - val f = isFile(path) - if (!f.isDirectory) { - throw IllegalArgumentException("$f is not a directory") - } - return f - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/utils/Scripts.kt b/src/main/kotlin/app/revanced/utils/Scripts.kt similarity index 93% rename from src/main/kotlin/app/revanced/cli/utils/Scripts.kt rename to src/main/kotlin/app/revanced/utils/Scripts.kt index 028c10b..eeef972 100644 --- a/src/main/kotlin/app/revanced/cli/utils/Scripts.kt +++ b/src/main/kotlin/app/revanced/utils/Scripts.kt @@ -1,9 +1,9 @@ -package app.revanced.cli.utils +package app.revanced.utils // TODO: make this a class with PACKAGE_NAME as argument, then use that everywhere. // make sure to remove the "const" from all the vals, they won't compile obviously. object Scripts { - private const val PACKAGE_NAME = "com.google.android.youtube" + private const val PACKAGE_NAME = "com.google.android.apps.youtube.music" private const val DATA_PATH = "/data/adb/ReVanced" const val APK_PATH = "/sdcard/base.apk" const val SCRIPT_PATH = "/sdcard/mount.sh" diff --git a/src/main/kotlin/app/revanced/cli/utils/DexReplacer.kt b/src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt similarity index 96% rename from src/main/kotlin/app/revanced/cli/utils/DexReplacer.kt rename to src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt index 171aa1f..9fe1cbd 100644 --- a/src/main/kotlin/app/revanced/cli/utils/DexReplacer.kt +++ b/src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt @@ -1,4 +1,4 @@ -package app.revanced.cli.utils +package app.revanced.utils.dex import lanchon.multidexlib2.BasicDexFileNamer import org.jf.dexlib2.writer.io.MemoryDataStore diff --git a/src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt b/src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt similarity index 96% rename from src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt rename to src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt index 9da9825..a848e62 100644 --- a/src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt +++ b/src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt @@ -1,4 +1,4 @@ -package app.revanced.cli.utils +package app.revanced.utils.patch import java.io.File import java.net.URL diff --git a/src/main/kotlin/app/revanced/cli/utils/Patches.kt b/src/main/kotlin/app/revanced/utils/patch/Patches.kt similarity index 94% rename from src/main/kotlin/app/revanced/cli/utils/Patches.kt rename to src/main/kotlin/app/revanced/utils/patch/Patches.kt index 65af28a..c561ba6 100644 --- a/src/main/kotlin/app/revanced/cli/utils/Patches.kt +++ b/src/main/kotlin/app/revanced/utils/patch/Patches.kt @@ -1,4 +1,4 @@ -package app.revanced.cli.utils +package app.revanced.utils.patch import app.revanced.patches.Index diff --git a/src/main/kotlin/app/revanced/cli/utils/signer/KeySet.kt b/src/main/kotlin/app/revanced/utils/signing/KeySet.kt similarity index 81% rename from src/main/kotlin/app/revanced/cli/utils/signer/KeySet.kt rename to src/main/kotlin/app/revanced/utils/signing/KeySet.kt index bbe0134..1eb9da8 100644 --- a/src/main/kotlin/app/revanced/cli/utils/signer/KeySet.kt +++ b/src/main/kotlin/app/revanced/utils/signing/KeySet.kt @@ -1,4 +1,4 @@ -package app.revanced.cli.utils.signer +package app.revanced.utils.signing import java.security.PrivateKey import java.security.cert.X509Certificate diff --git a/src/main/kotlin/app/revanced/cli/utils/signer/Signer.kt b/src/main/kotlin/app/revanced/utils/signing/Signer.kt similarity index 99% rename from src/main/kotlin/app/revanced/cli/utils/signer/Signer.kt rename to src/main/kotlin/app/revanced/utils/signing/Signer.kt index 936e279..ff56a29 100644 --- a/src/main/kotlin/app/revanced/cli/utils/signer/Signer.kt +++ b/src/main/kotlin/app/revanced/utils/signing/Signer.kt @@ -3,7 +3,7 @@ * Licensed under the Open Software License version 3.0 */ -package app.revanced.cli.utils.signer +package app.revanced.utils.signing import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo @@ -41,7 +41,7 @@ val PASSWORD = "revanced".toCharArray() // TODO: make it secure; random password /** * APK Signer. * @author Aliucord authors - * @author ReVanced Team + * @author ReVanced team */ object Signer { private fun newKeystore(out: File) {