diff --git a/build.gradle.kts b/build.gradle.kts index c9b9851..1b22dc1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,31 +24,25 @@ val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4" 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("info.picocli:picocli:+") - implementation("com.google.code.gson:gson:2.9.0") - implementation("me.tongfei:progressbar:0.9.3") + implementation("me.tongfei:progressbar:+") implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead. - implementation("org.bouncycastle:bcpkix-jdk15on:1.70") + 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.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 index a598750..f62f798 100644 --- a/src/main/kotlin/app/revanced/cli/Main.kt +++ b/src/main/kotlin/app/revanced/cli/Main.kt @@ -1,178 +1,7 @@ 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 +import picocli.CommandLine -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, - ) - } - } +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 new file mode 100644 index 0000000..29afa52 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -0,0 +1,84 @@ +package app.revanced.cli + +import app.revanced.patch.PatchLoader +import app.revanced.patch.Patches +import app.revanced.utils.adb.Adb +import picocli.CommandLine.* +import java.io.File + +@Command( + name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true +) +internal object MainCommand : Runnable { + @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 = ["-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 = ["-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 + + @Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"]) + internal var mergeFiles = listOf() + + @Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true) + internal lateinit var inputFile: File + + @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 { + PatchLoader.injectPatches(it) + Patches.loadPatches().forEach { + println(it().metadata) + } + } + return + } + + val patcher = app.revanced.patcher.Patcher( + inputFile, + cacheDirectory, + patchResources + ) + + Patcher.start(patcher) + + if (clean) { + File(cacheDirectory).deleteRecursively() + } + + val outputFile = File(outputPath) + + deploy?.let { + Adb( + outputFile, + patcher.packageName, + deploy!! + ).deploy() + } + + if (clean) outputFile.delete() + } +} \ 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..59a6b78 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/Patcher.kt @@ -0,0 +1,91 @@ +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 start(patcher: app.revanced.patcher.Patcher) { + // merge files like necessary integrations + patcher.addFiles(MainCommand.mergeFiles) + // add patches, but filter incompatible or excluded patches + patcher.addPatchesFiltered() + // apply patches + for ((meta, result) in patcher.applyPatches { + println("Applying $it.") + }) { + println("Applied ${meta.name}. The result was $result.") + } + + // 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 + } + }) { + + println("Skipping ${patch.metadata.name} due to incompatibility with current package $packageName.") + return@patch + } + + if (checkInclude && !MainCommand.includedPatches.contains(patch.metadata.shortName)) { + return@patch + } + + println("Adding ${patch.metadata.name}.") + includedPatches.add(patch) + + } + this.addPatches(includedPatches) + } + } + } +} 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/DexReplacer.kt b/src/main/kotlin/app/revanced/cli/utils/DexReplacer.kt deleted file mode 100644 index 171aa1f..0000000 --- a/src/main/kotlin/app/revanced/cli/utils/DexReplacer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.revanced.cli.utils - -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/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/cli/utils/Scripts.kt deleted file mode 100644 index 028c10b..0000000 --- a/src/main/kotlin/app/revanced/cli/utils/Scripts.kt +++ /dev/null @@ -1,34 +0,0 @@ -package app.revanced.cli.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 DATA_PATH = "/data/adb/ReVanced" - const val APK_PATH = "/sdcard/base.apk" - const val SCRIPT_PATH = "/sdcard/mount.sh" - - 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() - - 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" -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt b/src/main/kotlin/app/revanced/patch/PatchLoader.kt similarity index 84% rename from src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt rename to src/main/kotlin/app/revanced/patch/PatchLoader.kt index 9da9825..74ca2fd 100644 --- a/src/main/kotlin/app/revanced/cli/utils/PatchLoader.kt +++ b/src/main/kotlin/app/revanced/patch/PatchLoader.kt @@ -1,12 +1,12 @@ -package app.revanced.cli.utils +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/cli/utils/Patches.kt b/src/main/kotlin/app/revanced/patch/Patches.kt similarity index 77% rename from src/main/kotlin/app/revanced/cli/utils/Patches.kt rename to src/main/kotlin/app/revanced/patch/Patches.kt index 65af28a..7ba8147 100644 --- a/src/main/kotlin/app/revanced/cli/utils/Patches.kt +++ b/src/main/kotlin/app/revanced/patch/Patches.kt @@ -1,15 +1,15 @@ -package app.revanced.cli.utils +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/adb/Adb.kt b/src/main/kotlin/app/revanced/utils/adb/Adb.kt new file mode 100644 index 0000000..c03db28 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Adb.kt @@ -0,0 +1,94 @@ +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", 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_REVANCED}") + + // push patched file + device.copy(Constants.PATH_INIT_PUSH, apk) + // install apk + device.run(Constants.COMMAND_INSTALL_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()) + + // 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() + } + + 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 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..1b3af07 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Commands.kt @@ -0,0 +1,29 @@ +package app.revanced.utils.adb + +import se.vidstige.jadb.JadbDevice +import se.vidstige.jadb.RemoteFile +import se.vidstige.jadb.ShellProcessBuilder +import java.io.File + +internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { + if (su) { + return shellProcessBuilder("su -c \'$command\'") + } + + val args = command.split(" ") as ArrayList + val cmd = args.removeFirst() + + return shellProcessBuilder(cmd, *args.toTypedArray()) +} + +internal fun JadbDevice.run(command: String, su: Boolean = true): Int { + return this.buildCommand(command, su).start().waitFor() +} + +internal fun JadbDevice.copy(targetPath: String, file: File) { + push(file, RemoteFile(targetPath)) +} + +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 new file mode 100644 index 0000000..32f7bf1 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Constants.kt @@ -0,0 +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" + + // 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_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 = + """ + #!/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' } + mount -o bind ${'$'}base_path ${'$'}stock_path + """.trimIndent() +} \ 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 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) {