From f6d60a34608cbfc00b2b2004b9d1d70c17479464 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 1 May 2022 02:07:25 +0200 Subject: [PATCH 1/7] 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 (80%) 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 80% 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) { From 57af32208d6503376734a46df458cfd12c18fa59 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 4 May 2022 23:48:11 +0200 Subject: [PATCH 2/7] add: resource patcher Signed-off-by: oSumAtrIX --- .../kotlin/app/revanced/cli/MainCommand.kt | 97 +++++-------------- src/main/kotlin/app/revanced/cli/Patcher.kt | 96 ++++++++++++++++++ .../revanced/{utils => }/patch/PatchLoader.kt | 8 +- .../app/revanced/{utils => }/patch/Patches.kt | 8 +- src/main/kotlin/app/revanced/utils/Scripts.kt | 27 +++--- .../app/revanced/utils/dex/DexReplacer.kt | 31 ------ .../utils/filesystem/FileSystemUtils.kt | 66 +++++++++++++ 7 files changed, 206 insertions(+), 127 deletions(-) create mode 100644 src/main/kotlin/app/revanced/cli/Patcher.kt rename src/main/kotlin/app/revanced/{utils => }/patch/PatchLoader.kt (84%) rename src/main/kotlin/app/revanced/{utils => }/patch/Patches.kt (77%) delete mode 100644 src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt create mode 100644 src/main/kotlin/app/revanced/utils/filesystem/FileSystemUtils.kt diff --git a/src/main/kotlin/app/revanced/cli/MainCommand.kt b/src/main/kotlin/app/revanced/cli/MainCommand.kt index 7527e4e..17b8f2f 100644 --- a/src/main/kotlin/app/revanced/cli/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -1,40 +1,41 @@ 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 app.revanced.patch.PatchLoader +import app.revanced.patch.Patches import picocli.CommandLine import picocli.CommandLine.* import java.io.File @Command( - name = "ReVanced-CLI", - version = ["1.0.0"], - mixinStandardHelpOptions = true + name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true ) -object MainCommand : Runnable { +internal object MainCommand : Runnable { @Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"]) - var patchBundles = arrayOf() + internal var patchBundles = arrayOf() - @Parameters(paramLabel = "EXCLUDE", description = ["Which patches to exclude"]) - var excludedPatches = arrayOf() + @Parameters( + paramLabel = "INCLUDE", + description = ["Which patches to include. If none is specified, all compatible patches will be included."] + ) + internal var includedPatches = arrayOf() + + @Option(names = ["-c", "--cache"], description = ["Output resource cache directory"], required = true) + internal lateinit var cacheDirectory: String + + @Option(names = ["-r", "--resource-patcher"], description = ["Enable patching resources"]) + internal var patchResources: Boolean = false @Option(names = ["-l", "--list"], description = ["List patches only"]) - var listOnly: Boolean = false + internal var listOnly: Boolean = false @Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"]) - var mergeFiles = listOf() + internal var mergeFiles = listOf() @Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true) - lateinit var inputFile: File + internal lateinit var inputFile: File @Option(names = ["-o", "--out"], description = ["Output file path"], required = true) - lateinit var outputPath: String + internal lateinit var outputPath: String override fun run() { if (listOnly) { @@ -47,64 +48,10 @@ object MainCommand : Runnable { 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) + Patcher.run() } } -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) { +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.kt b/src/main/kotlin/app/revanced/cli/Patcher.kt new file mode 100644 index 0000000..26c78ce --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/Patcher.kt @@ -0,0 +1,96 @@ +package app.revanced.cli + +import app.revanced.patch.PatchLoader +import app.revanced.patch.Patches +import app.revanced.patcher.data.base.Data +import app.revanced.patcher.patch.base.Patch +import app.revanced.utils.filesystem.FileSystemUtils +import app.revanced.utils.signing.Signer +import java.io.File + +internal class Patcher { + internal companion object { + internal fun run() { + val patcher = app.revanced.patcher.Patcher( + MainCommand.inputFile, + MainCommand.cacheDirectory, + MainCommand.patchResources + ) + + // merge files like necessary integrations + patcher.addFiles(MainCommand.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(MainCommand.outputPath) + if (outFile.exists()) outFile.delete() + MainCommand.inputFile.copyTo(outFile) + + val zipFileSystem = FileSystemUtils(outFile) + + // replace all dex files + for ((name, data) in patcher.save()) { + zipFileSystem.replaceFile(name, data.data) + } + + if (MainCommand.patchResources) { + for (file in File(MainCommand.cacheDirectory).resolve("build/").listFiles().first().listFiles()) { + if (!file.isDirectory) { + zipFileSystem.replaceFile(file.name, file.readBytes()) + continue + } + zipFileSystem.replaceDirectory(file) + } + } + + // finally close the stream + zipFileSystem.close() + + // and sign the apk file + Signer.signApk(outFile) + } + + private fun app.revanced.patcher.Patcher.addPatchesFiltered() { + // TODO: get package metadata (outside of this method) for apk file which needs to be patched + val packageName = this.packageName + val packageVersion = this.packageVersion + + val checkInclude = MainCommand.includedPatches.isNotEmpty() + + MainCommand.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 (checkInclude && !MainCommand.includedPatches.contains(patch.metadata.shortName)) { + return@patch + } + + // TODO: report to stdout + includedPatches.add(patch) + + } + this.addPatches(includedPatches) + } + } + } +} diff --git a/src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt b/src/main/kotlin/app/revanced/patch/PatchLoader.kt similarity index 84% rename from src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt rename to src/main/kotlin/app/revanced/patch/PatchLoader.kt index a848e62..74ca2fd 100644 --- a/src/main/kotlin/app/revanced/utils/patch/PatchLoader.kt +++ b/src/main/kotlin/app/revanced/patch/PatchLoader.kt @@ -1,12 +1,12 @@ -package app.revanced.utils.patch +package app.revanced.patch import java.io.File import java.net.URL import java.net.URLClassLoader -class PatchLoader { - companion object { - fun injectPatches(file: File) { +internal class PatchLoader { + internal companion object { + internal fun injectPatches(file: File) { // This function will fail on Java 9 and above. try { val url = file.toURI().toURL() diff --git a/src/main/kotlin/app/revanced/utils/patch/Patches.kt b/src/main/kotlin/app/revanced/patch/Patches.kt similarity index 77% rename from src/main/kotlin/app/revanced/utils/patch/Patches.kt rename to src/main/kotlin/app/revanced/patch/Patches.kt index c561ba6..7ba8147 100644 --- a/src/main/kotlin/app/revanced/utils/patch/Patches.kt +++ b/src/main/kotlin/app/revanced/patch/Patches.kt @@ -1,15 +1,15 @@ -package app.revanced.utils.patch +package app.revanced.patch import app.revanced.patches.Index -class Patches { - companion object { +internal class Patches { + internal companion object { // You may ask yourself, "why do this?". // We do it like this, because we don't want the Index class // to be loaded while the dependency hasn't been injected yet. // You can see this as "controlled class loading". // Whenever this class is loaded (because it is invoked), all the imports // will be loaded too. We don't want to do this until we've injected the class. - fun loadPatches() = Index.patches + internal fun loadPatches() = Index.patches } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/Scripts.kt b/src/main/kotlin/app/revanced/utils/Scripts.kt index eeef972..31043fb 100644 --- a/src/main/kotlin/app/revanced/utils/Scripts.kt +++ b/src/main/kotlin/app/revanced/utils/Scripts.kt @@ -2,13 +2,13 @@ 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 { +internal object Scripts { 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" + internal const val APK_PATH = "/sdcard/base.apk" + internal const val SCRIPT_PATH = "/sdcard/mount.sh" - val MOUNT_SCRIPT = + internal val MOUNT_SCRIPT = """ base_path="$DATA_PATH/base.apk" stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' } @@ -21,14 +21,15 @@ object Scripts { mount -o bind ${'$'}base_path ${'$'}stock_path """.trimIndent() - const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME" + internal const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME" private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)" - const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\"" - const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\"" - const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\"" - const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh" - const val UNMOUNT_COMMAND = "su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\"" - const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\"" - const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\"" - const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1" + internal const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\"" + internal const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\"" + internal const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\"" + internal const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh" + internal const val UNMOUNT_COMMAND = + "su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\"" + internal const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\"" + internal const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\"" + internal const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1" } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt b/src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt deleted file mode 100644 index 9fe1cbd..0000000 --- a/src/main/kotlin/app/revanced/utils/dex/DexReplacer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.revanced.utils.dex - -import lanchon.multidexlib2.BasicDexFileNamer -import org.jf.dexlib2.writer.io.MemoryDataStore -import java.io.File -import java.nio.file.FileSystems -import java.nio.file.Files - -val NAMER = BasicDexFileNamer() - -object DexReplacer { - fun replaceDex(source: File, dexFiles: Map) { - FileSystems.newFileSystem( - source.toPath(), - null - ).use { fs -> - // Delete all classes?.dex files - Files.walk(fs.rootDirectories.first()).forEach { - if ( - it.toString().endsWith(".dex") && - NAMER.isValidName(it.fileName.toString()) - ) Files.delete(it) - } - // Write new dex files - dexFiles - .forEach { (dexName, dexData) -> - Files.write(fs.getPath("/$dexName"), dexData.data) - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/filesystem/FileSystemUtils.kt b/src/main/kotlin/app/revanced/utils/filesystem/FileSystemUtils.kt new file mode 100644 index 0000000..10559d8 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/filesystem/FileSystemUtils.kt @@ -0,0 +1,66 @@ +package app.revanced.utils.filesystem + +import java.io.Closeable +import java.io.File +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Files + +internal class FileSystemUtils( + file: File +) : Closeable { + private var fileSystem: FileSystem + + init { + fileSystem = FileSystems.newFileSystem( + file.toPath(), + null + ) + } + + private fun deleteDirectory(dirPath: String) { + val files = Files.walk(fileSystem.getPath("$dirPath/")) + + files + .sorted(Comparator.reverseOrder()) + .forEach { + + Files.delete(it) + } + + files.close() + } + + + internal fun replaceDirectory(replacement: File) { + if (!replacement.isDirectory) throw Exception("${replacement.name} is not a directory.") + + // FIXME: make this delete the directory recursively + //deleteDirectory(replacement.name) + //val path = Files.createDirectory(fileSystem.getPath(replacement.name)) + + val excludeFromPath = replacement.path.removeSuffix(replacement.name) + for (path in Files.walk(replacement.toPath())) { + val file = path.toFile() + if (file.isDirectory) { + val relativePath = path.toString().removePrefix(excludeFromPath) + val fileSystemPath = fileSystem.getPath(relativePath) + if (!Files.exists(fileSystemPath)) Files.createDirectory(fileSystemPath) + + continue + } + + replaceFile(path.toString().removePrefix(excludeFromPath), file.readBytes()) + } + } + + internal fun replaceFile(sourceFile: String, content: ByteArray) { + val path = fileSystem.getPath(sourceFile) + Files.deleteIfExists(path) + Files.write(path, content) + } + + override fun close() { + fileSystem.close() + } +} \ No newline at end of file From 03f2c5185e048da12d154865442387d7878fa9d1 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 4 May 2022 23:56:30 +0200 Subject: [PATCH 3/7] add: `wipe-after` option Signed-off-by: oSumAtrIX --- src/main/kotlin/app/revanced/cli/MainCommand.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/cli/MainCommand.kt b/src/main/kotlin/app/revanced/cli/MainCommand.kt index 17b8f2f..a15b07f 100644 --- a/src/main/kotlin/app/revanced/cli/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -15,7 +15,7 @@ internal object MainCommand : Runnable { @Parameters( paramLabel = "INCLUDE", - description = ["Which patches to include. If none is specified, all compatible patches will be included."] + description = ["Which patches to include. If none is specified, all compatible patches will be included"] ) internal var includedPatches = arrayOf() @@ -25,6 +25,10 @@ internal object MainCommand : Runnable { @Option(names = ["-r", "--resource-patcher"], description = ["Enable patching resources"]) internal var patchResources: Boolean = false + @Option(names = ["-w", "--wipe-after"], description = ["Wipe the temporal directory before exiting the patcher"]) + internal var wipe: Boolean = false + + @Option(names = ["-l", "--list"], description = ["List patches only"]) internal var listOnly: Boolean = false @@ -49,6 +53,9 @@ internal object MainCommand : Runnable { } Patcher.run() + + if (!wipe) return + File(cacheDirectory).deleteRecursively() } } From de2d29c46455c769196261c7a455a859231b62d2 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 5 May 2022 01:43:35 +0200 Subject: [PATCH 4/7] add: deploy to `adb` Signed-off-by: oSumAtrIX --- build.gradle.kts | 3 + .../kotlin/app/revanced/cli/MainCommand.kt | 26 +++++- src/main/kotlin/app/revanced/cli/Patcher.kt | 8 +- src/main/kotlin/app/revanced/utils/Scripts.kt | 35 -------- src/main/kotlin/app/revanced/utils/adb/Adb.kt | 85 +++++++++++++++++++ .../kotlin/app/revanced/utils/adb/Commands.kt | 25 ++++++ .../app/revanced/utils/adb/Constants.kt | 34 ++++++++ 7 files changed, 170 insertions(+), 46 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/utils/Scripts.kt create mode 100644 src/main/kotlin/app/revanced/utils/adb/Adb.kt create mode 100644 src/main/kotlin/app/revanced/utils/adb/Commands.kt create mode 100644 src/main/kotlin/app/revanced/utils/adb/Constants.kt diff --git a/build.gradle.kts b/build.gradle.kts index 730003e..b4e3ea6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,9 @@ dependencies { implementation("app.revanced:revanced-patcher:+") implementation(patchesDependency) implementation("info.picocli:picocli:+") + + implementation("me.tongfei:progressbar:+") + implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead. implementation("org.bouncycastle:bcpkix-jdk15on:+") } diff --git a/src/main/kotlin/app/revanced/cli/MainCommand.kt b/src/main/kotlin/app/revanced/cli/MainCommand.kt index a15b07f..57a4c36 100644 --- a/src/main/kotlin/app/revanced/cli/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -2,6 +2,7 @@ package app.revanced.cli import app.revanced.patch.PatchLoader import app.revanced.patch.Patches +import app.revanced.utils.adb.Adb import picocli.CommandLine import picocli.CommandLine.* import java.io.File @@ -10,15 +11,15 @@ import java.io.File name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true ) internal object MainCommand : Runnable { - @Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"]) - internal var patchBundles = arrayOf() - @Parameters( paramLabel = "INCLUDE", description = ["Which patches to include. If none is specified, all compatible patches will be included"] ) internal var includedPatches = arrayOf() + @Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"]) + internal var patchBundles = arrayOf() + @Option(names = ["-c", "--cache"], description = ["Output resource cache directory"], required = true) internal lateinit var cacheDirectory: String @@ -41,6 +42,10 @@ internal object MainCommand : Runnable { @Option(names = ["-o", "--out"], description = ["Output file path"], required = true) internal lateinit var outputPath: String + @Option(names = ["-d", "--deploy-on"], description = ["If specified, deploy to adb device with given name"]) + internal var deploy: String? = null + + override fun run() { if (listOnly) { patchBundles.forEach { @@ -52,10 +57,23 @@ internal object MainCommand : Runnable { return } - Patcher.run() + val patcher = app.revanced.patcher.Patcher( + inputFile, + cacheDirectory, + patchResources + ) + Patcher.start(patcher) if (!wipe) return File(cacheDirectory).deleteRecursively() + + deploy?.let { + Adb( + File(outputPath), + patcher.packageName, + deploy!! + ).deploy() + } } } diff --git a/src/main/kotlin/app/revanced/cli/Patcher.kt b/src/main/kotlin/app/revanced/cli/Patcher.kt index 26c78ce..548ef1c 100644 --- a/src/main/kotlin/app/revanced/cli/Patcher.kt +++ b/src/main/kotlin/app/revanced/cli/Patcher.kt @@ -10,13 +10,7 @@ import java.io.File internal class Patcher { internal companion object { - internal fun run() { - val patcher = app.revanced.patcher.Patcher( - MainCommand.inputFile, - MainCommand.cacheDirectory, - MainCommand.patchResources - ) - + internal fun start(patcher: app.revanced.patcher.Patcher) { // merge files like necessary integrations patcher.addFiles(MainCommand.mergeFiles) // add patches, but filter incompatible or excluded patches diff --git a/src/main/kotlin/app/revanced/utils/Scripts.kt b/src/main/kotlin/app/revanced/utils/Scripts.kt deleted file mode 100644 index 31043fb..0000000 --- a/src/main/kotlin/app/revanced/utils/Scripts.kt +++ /dev/null @@ -1,35 +0,0 @@ -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. -internal object Scripts { - private const val PACKAGE_NAME = "com.google.android.apps.youtube.music" - private const val DATA_PATH = "/data/adb/ReVanced" - internal const val APK_PATH = "/sdcard/base.apk" - internal const val SCRIPT_PATH = "/sdcard/mount.sh" - - internal val MOUNT_SCRIPT = - """ - base_path="$DATA_PATH/base.apk" - stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' } - umount -l ${'$'}stock_path - rm ${'$'}base_path - mv "$APK_PATH" ${'$'}base_path - chmod 644 ${'$'}base_path - chown system:system ${'$'}base_path - chcon u:object_r:apk_data_file:s0 ${'$'}base_path - mount -o bind ${'$'}base_path ${'$'}stock_path - """.trimIndent() - - internal const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME" - private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)" - internal const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\"" - internal const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\"" - internal const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\"" - internal const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh" - internal const val UNMOUNT_COMMAND = - "su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\"" - internal const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\"" - internal const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\"" - internal const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1" -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Adb.kt b/src/main/kotlin/app/revanced/utils/adb/Adb.kt new file mode 100644 index 0000000..a93a3cc --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Adb.kt @@ -0,0 +1,85 @@ +package app.revanced.utils.adb + +import se.vidstige.jadb.JadbConnection +import se.vidstige.jadb.JadbDevice +import java.io.File +import java.util.concurrent.Executors + +internal class Adb( + private val apk: File, + private val packageName: String, + deviceName: String, + private val logging: Boolean = true +) { + private val device: JadbDevice + + init { + device = JadbConnection().devices.find { it.serial == deviceName } + ?: throw IllegalArgumentException("No such device with name $deviceName") + + if (device.run("su -h") == 0) + throw IllegalArgumentException("Root required on $deviceName. Deploying failed.") + } + + internal fun deploy() { + // create revanced path + device.run(Constants.COMMAND_CREATE_DIR + Constants.PATH_DATA) + + // create mount script + device.createFile( + Constants.PATH_INIT_PUSH, + Constants.CONTENT_MOUNT_SCRIPT.replace(Constants.PLACEHOLDER, packageName) + ) + + // move the mount script to the revanced path + device.run(Constants.COMMAND_MOVE_MOUNT) + // make the mount script executable + device.run(Constants.COMMAND_CHMOD_MOUNT + Constants.NAME_MOUNT_SCRIPT) + + // push patched file + device.copy(Constants.PATH_INIT_PUSH, apk) + // move patched file to revanced path + device.run(Constants.COMMAND_MOVE_BASE) + + // kill, mount & run app + device.run(Constants.COMMAND_KILL_APP.replace(Constants.PLACEHOLDER, packageName)) + device.run(Constants.COMMAND_MOUNT) + device.run(Constants.COMMAND_RUN_APP.replace(Constants.PLACEHOLDER, packageName)) + + // log the app + log() + + // unmount it, after it closes + device.run(Constants.COMMAND_UNMOUNT.replace(Constants.PLACEHOLDER, packageName)) + } + + 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.replace(Constants.PLACEHOLDER, packageName)) + .redirectOutput(pipe) + .redirectError(pipe) + .useExecutor(executor) + .start() + + Thread.sleep(250) // 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 state of app", e) + } + } + println("App closed, continuing.") + process.destroy() + executor.shutdown() + } +} \ 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 new file mode 100644 index 0000000..af23c27 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Commands.kt @@ -0,0 +1,25 @@ +package app.revanced.utils.adb + +import se.vidstige.jadb.JadbDevice +import se.vidstige.jadb.RemoteFile +import se.vidstige.jadb.ShellProcessBuilder +import java.io.File + +internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { + val args = command.split(" ") as ArrayList + val cmd = args.removeFirst() + + return shellProcessBuilder(cmd, *args.toTypedArray()) +} + +internal fun JadbDevice.run(command: String, su: Boolean = true): Int { + return this.buildCommand(command).start().waitFor() +} + +internal fun JadbDevice.copy(targetPath: String, file: File) { + push(file, RemoteFile(targetPath)) +} + +internal fun JadbDevice.createFile(targetFile: String, content: String, su: Boolean = true) { + push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) +} \ 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 new file mode 100644 index 0000000..39b74c8 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Constants.kt @@ -0,0 +1,34 @@ +package app.revanced.utils.adb + +internal object Constants { + internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME" + + internal const val NAME_MOUNT_SCRIPT = "mount.sh" + + internal const val PATH_DATA = "/data/adb/revanced/" + internal const val PATH_INIT_PUSH = "/sdcard/revanced" + + internal const val COMMAND_PID_OF = "pidof -s " + internal const val COMMAND_CREATE_DIR = "mkdir -p " + internal const val COMMAND_MOVE_BASE = "mv $PATH_INIT_PUSH $PATH_DATA/base.apk" + internal const val COMMAND_MOVE_MOUNT = "mv $PATH_INIT_PUSH $PATH_DATA/$NAME_MOUNT_SCRIPT" + internal const val COMMAND_CHMOD_MOUNT = "chmod +x $PATH_DATA" + internal const val COMMAND_MOUNT = "./$PATH_DATA/$NAME_MOUNT_SCRIPT" + internal const val COMMAND_UNMOUNT = "umount -l $(pm path $PLACEHOLDER | grep base | sed 's/package://g')" + internal const val COMMAND_LOGCAT = "logcat -c && logcat --pid=$($COMMAND_PID_OF $PLACEHOLDER)" + internal const val COMMAND_RUN_APP = "monkey -p $PLACEHOLDER 1" + internal const val COMMAND_KILL_APP = "kill \$($COMMAND_PID_OF $PLACEHOLDER)" + + internal val CONTENT_MOUNT_SCRIPT = + """ + base_path="$PATH_DATA/base.apk" + stock_path=${'$'}{ pm path $PLACEHOLDER | grep base | sed 's/package://g' } + umount -l ${'$'}stock_path + rm ${'$'}base_path + 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 + mount -o bind ${'$'}base_path ${'$'}stock_path + """.trimIndent() +} \ No newline at end of file From 6ffba3ef0a089c01fd31b667a37a27e77186bbbd Mon Sep 17 00:00:00 2001 From: Canny1913 <94744045+Canny1913@users.noreply.github.com> Date: Thu, 5 May 2022 20:19:59 +0300 Subject: [PATCH 5/7] fix: gradle build script Signed-off-by: oSumAtrIX --- build.gradle.kts | 4 ++-- src/main/kotlin/app/revanced/cli/Main.kt | 7 +++++++ src/main/kotlin/app/revanced/cli/MainCommand.kt | 5 ----- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/app/revanced/cli/Main.kt diff --git a/build.gradle.kts b/build.gradle.kts index 730003e..25815c9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,7 @@ repositories { } } -val patchesDependency = "app.revanced:revanced-patches:+" +val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4" dependencies { implementation(kotlin("stdlib")) @@ -39,7 +39,7 @@ tasks { exclude(dependency(patchesDependency)) } manifest { - attributes("Main-Class" to "app.revanced.cli.Main") + attributes("Main-Class" to "app.revanced.cli.MainKt") 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 new file mode 100644 index 0000000..f62f798 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/Main.kt @@ -0,0 +1,7 @@ +package app.revanced.cli + +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/MainCommand.kt b/src/main/kotlin/app/revanced/cli/MainCommand.kt index a15b07f..0abfe89 100644 --- a/src/main/kotlin/app/revanced/cli/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -2,7 +2,6 @@ package app.revanced.cli import app.revanced.patch.PatchLoader import app.revanced.patch.Patches -import picocli.CommandLine import picocli.CommandLine.* import java.io.File @@ -57,8 +56,4 @@ internal object MainCommand : Runnable { if (!wipe) return File(cacheDirectory).deleteRecursively() } -} - -internal fun main(args: Array) { - CommandLine(MainCommand).execute(*args) } \ No newline at end of file From f9b987e858292332a4b99e4e4280647425b8c0b8 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 6 May 2022 03:00:02 +0200 Subject: [PATCH 6/7] fix: deploy to `adb` Signed-off-by: oSumAtrIX --- .../kotlin/app/revanced/cli/MainCommand.kt | 31 +++++---- src/main/kotlin/app/revanced/utils/adb/Adb.kt | 59 ++++++++++------- .../kotlin/app/revanced/utils/adb/Commands.kt | 8 ++- .../app/revanced/utils/adb/Constants.kt | 65 +++++++++++++------ 4 files changed, 103 insertions(+), 60 deletions(-) diff --git a/src/main/kotlin/app/revanced/cli/MainCommand.kt b/src/main/kotlin/app/revanced/cli/MainCommand.kt index 659f31d..29afa52 100644 --- a/src/main/kotlin/app/revanced/cli/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -2,7 +2,7 @@ package app.revanced.cli import app.revanced.patch.PatchLoader import app.revanced.patch.Patches -import picocli.CommandLine +import app.revanced.utils.adb.Adb import picocli.CommandLine.* import java.io.File @@ -10,24 +10,26 @@ import java.io.File name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true ) internal object MainCommand : Runnable { - @Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"]) - internal var patchBundles = arrayOf() - @Parameters( paramLabel = "INCLUDE", description = ["Which patches to include. If none is specified, all compatible patches will be included"] ) internal var includedPatches = arrayOf() - @Option(names = ["-c", "--cache"], description = ["Output resource cache directory"], required = true) + @Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"]) + internal var patchBundles = arrayOf() + + @Option(names = ["-t", "--temp-dir"], description = ["Temporal resource cache directory"], required = true) internal lateinit var cacheDirectory: String @Option(names = ["-r", "--resource-patcher"], description = ["Enable patching resources"]) internal var patchResources: Boolean = false - @Option(names = ["-w", "--wipe-after"], description = ["Wipe the temporal directory before exiting the patcher"]) - internal var wipe: Boolean = false - + @Option( + names = ["-c", "--clean"], + description = ["Clean the temporal resource cache directory. This will be done anyways when running the patcher"] + ) + internal var clean: Boolean = false @Option(names = ["-l", "--list"], description = ["List patches only"]) internal var listOnly: Boolean = false @@ -44,7 +46,6 @@ internal object MainCommand : Runnable { @Option(names = ["-d", "--deploy-on"], description = ["If specified, deploy to adb device with given name"]) internal var deploy: String? = null - override fun run() { if (listOnly) { patchBundles.forEach { @@ -61,17 +62,23 @@ internal object MainCommand : Runnable { cacheDirectory, patchResources ) + Patcher.start(patcher) - if (!wipe) return - File(cacheDirectory).deleteRecursively() + if (clean) { + File(cacheDirectory).deleteRecursively() + } + + val outputFile = File(outputPath) deploy?.let { Adb( - File(outputPath), + outputFile, patcher.packageName, deploy!! ).deploy() } + + if (clean) outputFile.delete() } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Adb.kt b/src/main/kotlin/app/revanced/utils/adb/Adb.kt index a93a3cc..c03db28 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Adb.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Adb.kt @@ -17,40 +17,49 @@ internal class Adb( device = JadbConnection().devices.find { it.serial == deviceName } ?: throw IllegalArgumentException("No such device with name $deviceName") - if (device.run("su -h") == 0) + if (device.run("su -h", false) != 0) throw IllegalArgumentException("Root required on $deviceName. Deploying failed.") } + private fun String.replacePlaceholder(): String { + return this.replace(Constants.PLACEHOLDER, packageName) + } + internal fun deploy() { // create revanced path - device.run(Constants.COMMAND_CREATE_DIR + Constants.PATH_DATA) - - // create mount script - device.createFile( - Constants.PATH_INIT_PUSH, - Constants.CONTENT_MOUNT_SCRIPT.replace(Constants.PLACEHOLDER, packageName) - ) - - // move the mount script to the revanced path - device.run(Constants.COMMAND_MOVE_MOUNT) - // make the mount script executable - device.run(Constants.COMMAND_CHMOD_MOUNT + Constants.NAME_MOUNT_SCRIPT) + device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}") // push patched file device.copy(Constants.PATH_INIT_PUSH, apk) - // move patched file to revanced path - device.run(Constants.COMMAND_MOVE_BASE) + // install apk + device.run(Constants.COMMAND_INSTALL_APK.replacePlaceholder()) - // kill, mount & run app - device.run(Constants.COMMAND_KILL_APP.replace(Constants.PLACEHOLDER, packageName)) - device.run(Constants.COMMAND_MOUNT) - device.run(Constants.COMMAND_RUN_APP.replace(Constants.PLACEHOLDER, packageName)) + // push mount script + device.createFile( + Constants.PATH_INIT_PUSH, + Constants.CONTENT_MOUNT_SCRIPT.replacePlaceholder() + ) + // install mount script + device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder()) + + // push umount script + device.createFile( + Constants.PATH_INIT_PUSH, + Constants.CONTENT_UMOUNT_SCRIPT.replacePlaceholder() + ) + // install mount script + device.run(Constants.COMMAND_INSTALL_UMOUNT.replacePlaceholder()) + + // unmount the apk for sanity + device.run(Constants.PATH_UMOUNT.replacePlaceholder()) + // mount the apk + device.run(Constants.PATH_MOUNT.replacePlaceholder()) + + // relaunch app + device.run(Constants.COMMAND_RESTART.replacePlaceholder()) // log the app log() - - // unmount it, after it closes - device.run(Constants.COMMAND_UNMOUNT.replace(Constants.PLACEHOLDER, packageName)) } private fun log() { @@ -61,16 +70,16 @@ internal class Adb( ProcessBuilder.Redirect.PIPE } - val process = device.buildCommand(Constants.COMMAND_LOGCAT.replace(Constants.PLACEHOLDER, packageName)) + val process = device.buildCommand(Constants.COMMAND_LOGCAT.replacePlaceholder()) .redirectOutput(pipe) .redirectError(pipe) .useExecutor(executor) .start() - Thread.sleep(250) // give the app some time to start up. + Thread.sleep(500) // give the app some time to start up. while (true) { try { - while (device.run(Constants.COMMAND_PID_OF + packageName) == 0) { + while (device.run("${Constants.COMMAND_PID_OF} $packageName") == 0) { Thread.sleep(1000) } break diff --git a/src/main/kotlin/app/revanced/utils/adb/Commands.kt b/src/main/kotlin/app/revanced/utils/adb/Commands.kt index af23c27..1b3af07 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Commands.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Commands.kt @@ -6,6 +6,10 @@ import se.vidstige.jadb.ShellProcessBuilder import java.io.File internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { + if (su) { + return shellProcessBuilder("su -c \'$command\'") + } + val args = command.split(" ") as ArrayList val cmd = args.removeFirst() @@ -13,13 +17,13 @@ internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): Shell } internal fun JadbDevice.run(command: String, su: Boolean = true): Int { - return this.buildCommand(command).start().waitFor() + return this.buildCommand(command, su).start().waitFor() } internal fun JadbDevice.copy(targetPath: String, file: File) { push(file, RemoteFile(targetPath)) } -internal fun JadbDevice.createFile(targetFile: String, content: String, su: Boolean = true) { +internal fun JadbDevice.createFile(targetFile: String, content: String) { push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) } \ 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 39b74c8..32f7bf1 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Constants.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Constants.kt @@ -1,34 +1,57 @@ 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 NAME_MOUNT_SCRIPT = "mount.sh" - - internal const val PATH_DATA = "/data/adb/revanced/" - internal const val PATH_INIT_PUSH = "/sdcard/revanced" - - internal const val COMMAND_PID_OF = "pidof -s " - internal const val COMMAND_CREATE_DIR = "mkdir -p " - internal const val COMMAND_MOVE_BASE = "mv $PATH_INIT_PUSH $PATH_DATA/base.apk" - internal const val COMMAND_MOVE_MOUNT = "mv $PATH_INIT_PUSH $PATH_DATA/$NAME_MOUNT_SCRIPT" - internal const val COMMAND_CHMOD_MOUNT = "chmod +x $PATH_DATA" - internal const val COMMAND_MOUNT = "./$PATH_DATA/$NAME_MOUNT_SCRIPT" - internal const val COMMAND_UNMOUNT = "umount -l $(pm path $PLACEHOLDER | grep base | sed 's/package://g')" + // 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 --pid=$($COMMAND_PID_OF $PLACEHOLDER)" - internal const val COMMAND_RUN_APP = "monkey -p $PLACEHOLDER 1" - internal const val COMMAND_KILL_APP = "kill \$($COMMAND_PID_OF $PLACEHOLDER)" + internal const val COMMAND_RESTART = "monkey -p $PLACEHOLDER 1 && kill ${'$'}($COMMAND_PID_OF $PLACEHOLDER)" + // default mount file name + private const val NAME_MOUNT_SCRIPT = "mount_$PLACEHOLDER.sh" + + // initial directory to push files to via adb push + internal const val PATH_INIT_PUSH = "/sdcard/revanced.delete" + + // revanced path + internal const val PATH_REVANCED = "/data/adb/revanced/" + + // revanced apk path + private const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk" + + // (un)mount script paths + internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT" + internal const val PATH_UMOUNT = "/data/adb/post-fs-data.d/un$NAME_MOUNT_SCRIPT" + + // move to revanced apk path & set permissions + internal const val COMMAND_INSTALL_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" + + // install mount script & set permissions + internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT" + + // install umount script & set permissions + internal const val COMMAND_INSTALL_UMOUNT = "mv $PATH_INIT_PUSH $PATH_UMOUNT && $COMMAND_CHMOD_MOUNT $PATH_UMOUNT" + + // unmount script + internal val CONTENT_UMOUNT_SCRIPT = + """ + #!/system/bin/sh + while read line; do echo ${'$'}{line} | grep $PLACEHOLDER | awk '{print ${'$'}2}' | xargs umount -l; done< /proc/mounts + """.trimIndent() + + // mount script internal val CONTENT_MOUNT_SCRIPT = """ - base_path="$PATH_DATA/base.apk" + #!/system/bin/sh + while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done + + base_path="$PATH_REVANCED_APP" stock_path=${'$'}{ pm path $PLACEHOLDER | grep base | sed 's/package://g' } - umount -l ${'$'}stock_path - rm ${'$'}base_path - 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 mount -o bind ${'$'}base_path ${'$'}stock_path """.trimIndent() } \ No newline at end of file From 59c423bea01b8c8de8f5bec5ae438eb38a684d1c Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 6 May 2022 03:09:00 +0200 Subject: [PATCH 7/7] add: proper console output Signed-off-by: oSumAtrIX --- src/main/kotlin/app/revanced/cli/Patcher.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/app/revanced/cli/Patcher.kt b/src/main/kotlin/app/revanced/cli/Patcher.kt index 548ef1c..59a6b78 100644 --- a/src/main/kotlin/app/revanced/cli/Patcher.kt +++ b/src/main/kotlin/app/revanced/cli/Patcher.kt @@ -16,10 +16,10 @@ internal class Patcher { // add patches, but filter incompatible or excluded patches patcher.addPatchesFiltered() // apply patches - for (patchResult in patcher.applyPatches { - println("Applying: $it") + for ((meta, result) in patcher.applyPatches { + println("Applying $it.") }) { - println(patchResult) + println("Applied ${meta.name}. The result was $result.") } // write output file @@ -71,7 +71,8 @@ internal class Patcher { it == packageVersion } }) { - // TODO: report to stdout + + println("Skipping ${patch.metadata.name} due to incompatibility with current package $packageName.") return@patch } @@ -79,7 +80,7 @@ internal class Patcher { return@patch } - // TODO: report to stdout + println("Adding ${patch.metadata.name}.") includedPatches.add(patch) }